// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package checkinparse contains functions to parse checkin report into batterystats proto.
package checkinparse

import (
	"errors"
	"fmt"
	"math"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/golang/protobuf/proto"

	"github.com/google/battery-historian/build"
	"github.com/google/battery-historian/checkinutil"
	"github.com/google/battery-historian/historianutils"
	"github.com/google/battery-historian/packageutils"
	"github.com/google/battery-historian/sliceparse"

	bspb "github.com/google/battery-historian/pb/batterystats_proto"
	sessionpb "github.com/google/battery-historian/pb/session_proto"
	usagepb "github.com/google/battery-historian/pb/usagestats_proto"
)

const (
	// minimum number of fields any type of battery stats have
	minNumFields = 4
	// Current range of supported/expected checkin versions.
	minParseReportVersion = 11
	maxParseReportVersion = 21
)

// Possible battery stats categories generated by on device java code.
const (
	info         = "i"
	sinceCharged = "l" // SINCE_CHARGED: Since last charged: The only reliable value.
	current      = "c" // Deprecated
	unplugged    = "u" // Deprecated: SINCE_UNPLUGGED: Very unreliable and soon to be removed.
)

// String representations of categories the parsing code handles. Contains all
// categories defined in frameworks/base/core/java/android/os/BatteryStats.java
// unless explicitly stated.
const (
	versionData                   = "vers"
	uidData                       = "uid"
	wakeupAlarmData               = "wua"
	apkData                       = "apk"
	processData                   = "pr"
	cpuData                       = "cpu"
	sensorData                    = "sr"
	vibratorData                  = "vib"
	foregroundData                = "fg"
	stateTimeData                 = "st"
	wakelockData                  = "wl"
	syncData                      = "sy"
	jobData                       = "jb"
	kernelWakelockData            = "kwl"
	wakeupReasonData              = "wr"
	networkData                   = "nt"
	userActivityData              = "ua"
	batteryData                   = "bt"
	batteryDischargeData          = "dc"
	batteryLevelData              = "lv"
	globalWifiData                = "gwfl"
	globalWifiControllerData      = "gwfcd"
	wifiControllerData            = "wfcd"
	wifiData                      = "wfl"
	bluetoothControllerData       = "ble"
	bluetoothMiscData             = "blem"
	globalBluetoothControllerData = "gble" // Previously globalBluetoothData
	miscData                      = "m"
	modemControllerData           = "mcd"
	globalModemControllerData     = "gmcd"
	globalNetworkData             = "gn"
	// HISTORY_STRING_POOL (hsp) is not included in the checkin log.
	// HISTORY_DATA (h) is not included in the checkin log.
	screenBrightnessData        = "br"
	signalStrengthTimeData      = "sgt"
	signalScanningTimeData      = "sst"
	signalStrengthCountData     = "sgc"
	dataConnectionTimeData      = "dct"
	dataConnectionCountData     = "dcc"
	wifiStateTimeData           = "wst"
	wifiStateCountData          = "wsc"
	wifiSupplStateTimeData      = "wsst"
	wifiSupplStateCountData     = "wssc"
	wifiSignalStrengthTimeData  = "wsgt"
	wifiSignalStrengthCountData = "wsgc"
	bluetoothStateTimeData      = "bst"
	bluetoothStateCountData     = "bsc"
	powerUseSummaryData         = "pws"
	powerUseItemData            = "pwi"
	dischargeStepData           = "dsd"
	chargeStepData              = "csd"
	dischargeTimeRemainData     = "dtr"
	chargeTimeRemainData        = "ctr"
	flashlightData              = "fla"
	cameraData                  = "cam"
	videoData                   = "vid"
	audioData                   = "aud"
)

var (
	powerUseItemNameMap = map[string]bspb.BatteryStats_System_PowerUseItem_Name{
		"idle":       bspb.BatteryStats_System_PowerUseItem_IDLE,
		"cell":       bspb.BatteryStats_System_PowerUseItem_CELL,
		"phone":      bspb.BatteryStats_System_PowerUseItem_PHONE,
		"wifi":       bspb.BatteryStats_System_PowerUseItem_WIFI,
		"blue":       bspb.BatteryStats_System_PowerUseItem_BLUETOOTH,
		"scrn":       bspb.BatteryStats_System_PowerUseItem_SCREEN,
		"uid":        bspb.BatteryStats_System_PowerUseItem_APP,
		"user":       bspb.BatteryStats_System_PowerUseItem_USER,
		"unacc":      bspb.BatteryStats_System_PowerUseItem_UNACCOUNTED,
		"over":       bspb.BatteryStats_System_PowerUseItem_OVERCOUNTED,
		"???":        bspb.BatteryStats_System_PowerUseItem_DEFAULT,
		"flashlight": bspb.BatteryStats_System_PowerUseItem_FLASHLIGHT,
	}

	// sharedUIDLabelMap contains a mapping of known shared UID labels to predefined group names.
	sharedUIDLabelMap = map[string]string{
		"android.media":                          "MEDIA",
		"android.uid.bluetooth":                  "BLUETOOTH",
		"android.uid.nfc":                        "NFC",
		"android.uid.phone":                      "RADIO",             // Associated with UID 1001.
		"android.uid.shared":                     "CONTACTS_PROVIDER", // "com.android.providers.contacts" is a prominent member of the UID group
		"android.uid.shell":                      "SHELL",
		"android.uid.system":                     "ANDROID_SYSTEM",
		"android.uid.systemui":                   "SYSTEM_UI",
		"com.google.android.calendar.uid.shared": "GOOGLE_CALENDAR",
		"com.google.uid.shared":                  "GOOGLE_SERVICES",
	}

	// TODO: get rid of packageNameToSharedUIDMap
	// Contains a mapping of known packages with their shared UIDs.
	packageNameToSharedUIDMap = map[string]string{
		// com.google.uid.shared
		"com.google.android.apps.gtalkservice":    "GOOGLE_SERVICES",
		"com.google.android.backuptransport":      "GOOGLE_SERVICES",
		"com.google.android.gms":                  "GOOGLE_SERVICES",
		"com.google.android.gms.car.userfeedback": "GOOGLE_SERVICES",
		"com.google.android.googleapps":           "GOOGLE_SERVICES",
		"com.google.android.gsf":                  "GOOGLE_SERVICES",
		"com.google.android.gsf.login":            "GOOGLE_SERVICES",
		"com.google.android.gsf.notouch":          "GOOGLE_SERVICES",
		"com.google.android.providers.gmail":      "GOOGLE_SERVICES",
		"com.google.android.sss.authbridge":       "GOOGLE_SERVICES",
		"com.google.gch.gateway":                  "GOOGLE_SERVICES",

		// com.google.android.calendar.uid.shared
		"com.android.calendar":                     "GOOGLE_CALENDAR",
		"com.google.android.calendar":              "GOOGLE_CALENDAR",
		"com.google.android.syncadapters.calendar": "GOOGLE_CALENDAR",

		// android.uid.system
		"android":                            "ANDROID_SYSTEM",
		"com.android.changesettings":         "ANDROID_SYSTEM",
		"com.android.inputdevices":           "ANDROID_SYSTEM",
		"com.android.keychain":               "ANDROID_SYSTEM",
		"com.android.location.fused":         "ANDROID_SYSTEM",
		"com.android.providers.settings":     "ANDROID_SYSTEM",
		"com.android.settings":               "ANDROID_SYSTEM",
		"com.google.android.canvas.settings": "ANDROID_SYSTEM",
		"com.lge.SprintHiddenMenu":           "ANDROID_SYSTEM",
		"com.nvidia.tegraprofiler.security":  "ANDROID_SYSTEM",
		"com.qualcomm.atfwd":                 "ANDROID_SYSTEM",
		"com.qualcomm.display":               "ANDROID_SYSTEM",

		// android.uid.phone, associated with UID 1001
		"com.android.phone":                    "RADIO",
		"com.android.providers.telephony":      "RADIO",
		"com.android.sdm.plugins.connmo":       "RADIO",
		"com.android.sdm.plugins.dcmo":         "RADIO",
		"com.android.sdm.plugins.sprintdm":     "RADIO",
		"com.htc.android.qxdm2sd":              "RADIO",
		"com.android.mms.service":              "RADIO",
		"com.android.server.telecom":           "RADIO",
		"com.android.sprint.lifetimedata":      "RADIO",
		"com.android.stk":                      "RADIO",
		"com.motorola.service.ims":             "RADIO",
		"com.qualcomm.qti.imstestrunner":       "RADIO",
		"com.qualcomm.qti.rcsbootstraputil":    "RADIO",
		"com.qualcomm.qti.rcsimsbootstraputil": "RADIO",
		"com.qualcomm.qcrilmsgtunnel":          "RADIO",
		"com.qualcomm.shutdownlistner":         "RADIO",
		"org.codeaurora.ims":                   "RADIO",
		"com.asus.atcmd":                       "RADIO",
		"com.mediatek.imeiwriter":              "RADIO",

		// android.uid.nfc
		"com.android.nfc":                 "NFC",
		"com.android.nfc3":                "NFC",
		"com.google.android.uiccwatchdog": "NFC",

		// android.uid.shared
		"com.android.contacts":                 "CONTACTS_PROVIDER",
		"com.android.providers.applications":   "CONTACTS_PROVIDER",
		"com.android.providers.contacts":       "CONTACTS_PROVIDER",
		"com.android.providers.userdictionary": "CONTACTS_PROVIDER",

		// android.media
		"com.android.gallery":                "MEDIA",
		"com.android.providers.downloads":    "MEDIA",
		"com.android.providers.downloads.ui": "MEDIA",
		"com.android.providers.drm":          "MEDIA",
		"com.android.providers.media":        "MEDIA",

		// android.uid.systemui
		"com.android.keyguard": "SYSTEM_UI",
		"com.android.systemui": "SYSTEM_UI",

		// android.uid.bluetooth
		"com.android.bluetooth": "BLUETOOTH",

		// android.uid.shell
		"com.android.shell": "SHELL",
	}

	// KnownUIDs lists all constant UIDs defined in system/core/include/private/android_filesystem_config.h.
	// GIDs are excluded. These should never be renumbered.
	KnownUIDs = map[int32]string{
		0:    "ROOT",           // traditional unix root user
		1000: "ANDROID_SYSTEM", // system server
		1001: "RADIO",          // telephony subsystem, RIL
		1002: "BLUETOOTH",      // bluetooth subsystem
		1003: "GRAPHICS",       // graphics devices
		1004: "INPUT",          // input devices
		1005: "AUDIO",          // audio devices
		1006: "CAMERA",         // camera devices
		1007: "LOG",            // log devices
		1008: "COMPASS",        // compass device
		1009: "MOUNT",          // mountd socket
		1010: "WIFI",           // wifi subsystem
		1011: "ADB",            // android debug bridge (adbd)
		1012: "INSTALL",        // group for installing packages
		1013: "MEDIA",          // mediaserver process
		1014: "DHCP",           // dhcp client
		1015: "SDCARD_RW",      // external storage write access
		1016: "VPN",            // vpn system
		1017: "KEYSTORE",       // keystore subsystem
		1018: "USB",            // USB devices
		1019: "DRM",            // DRM server
		1020: "MDNSR",          // MulticastDNSResponder (service discovery)
		1021: "GPS",            // GPS daemon
		// 1022 is deprecated and unused
		1023: "MEDIA_RW", // internal media storage write access
		1024: "MTP",      // MTP USB driver access
		// 1025 is deprecated and unused
		1026: "DRMRPC",          // group for drm rpc
		1027: "NFC",             // nfc subsystem
		1028: "SDCARD_R",        // external storage read access
		1029: "CLAT",            // clat part of nat464
		1030: "LOOP_RADIO",      // loop radio devices
		1031: "MEDIA_DRM",       // MediaDrm plugins
		1032: "PACKAGE_INFO",    // access to installed package details
		1033: "SDCARD_PICS",     // external storage photos access
		1034: "SDCARD_AV",       // external storage audio/video access
		1035: "SDCARD_ALL",      // access all users external storage
		1036: "LOGD",            // log daemon
		1037: "SHARED_RELRO",    // creator of shared GNU RELRO files
		1038: "DBUS",            // dbus-daemon IPC broker process
		1039: "TLSDATE",         // tlsdate unprivileged user
		1040: "MEDIA_EX",        // mediaextractor process
		1041: "AUDIOSERVER",     // audioserver process
		1042: "METRICS_COLL",    // metrics_collector process
		1043: "METRICSD",        // metricsd process
		1044: "WEBSERV",         // webservd process
		1045: "DEBUGGERD",       // debuggerd unprivileged user
		1046: "MEDIA_CODEC",     // mediacodec process
		1047: "CAMERASERVER",    // cameraserver process
		1048: "FIREWALL",        // firewalld process
		1049: "TRUNKS",          // trunksd process (TPM daemon)
		1050: "NVRAM",           // Access-controlled NVRAM
		1051: "DNS",             // DNS resolution daemon (system: netd)
		1052: "DNS_TETHER",      // DNS resolution daemon (tether: dnsmasq)
		1053: "WEBVIEW_ZYGOTE",  // WebView zygote process
		1054: "VEHICLE_NETWORK", // Vehicle network service
		1055: "MEDIA_AUDIO",     // GID for audio files on internal media storage
		1056: "MEDIA_VIDEO",     // GID for video files on internal media storage
		1057: "MEDIA_IMAGE",     // GID for image files on internal media storage
		1058: "TOMBSTONED",      // tombstoned user
		1059: "MEDIA_OBB",       // GID for OBB files on internal media storage
		1060: "ESE",             // embedded secure element (eSE) subsystem
		1061: "OTA_UPDATE",      // resource tracking UID for OTA updates

		2000: "SHELL", // adb and debug shell user
		2001: "CACHE", // cache access
		2002: "DIAG",  // access to diagnostic resources

		// 2900-2999 is reserved for OEM

		// The 3000 series are intended for use as supplemental group id's only.
		// They indicate special Android capabilities that the kernel is aware of.
		3001: "NET_BT_ADMIN", // bluetooth: create any socket
		3002: "NET_BT",       // bluetooth: create sco, rfcomm or l2cap sockets
		3003: "INET",         // can create AF_INET and AF_INET6 sockets
		3004: "NET_RAW",      // can create raw INET sockets
		3005: "NET_ADMIN",    // can configure interfaces and routing tables
		3006: "NET_BW_STATS", // read bandwidth statistics
		3007: "NET_BW_ACCT",  // change bandwidth statistics accounting
		3008: "NET_BT_STACK", // bluetooth: access config files
		3009: "READPROC",     // allow /proc read access
		3010: "WAKELOCK",     // allow system wakelock read/write access

		// 5000-5999 is reserved for OEM

		9997: "EVERYBODY", // shared between all apps in the same profile
		9998: "MISC",      // access to misc storage
		9999: "NOBODY",
	}

	// BatteryStatsIDMap contains a list of all the fields in BatteryStats_* messages that are the IDs for the message.
	BatteryStatsIDMap = map[reflect.Type]map[int]bool{
		// App fields
		reflect.TypeOf(&bspb.BatteryStats_App_Apk_Service{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_App_Process{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_App_ScheduledJob{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_App_Sensor{}): {
			0: true, // number
		},
		reflect.TypeOf(&bspb.BatteryStats_App_Sync{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_App_UserActivity{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_App_Wakelock{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_App_WakeupAlarm{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_ControllerActivity_TxLevel{}): {
			0: true, // level
		},
		// System fields
		reflect.TypeOf(&bspb.BatteryStats_System_Battery{}): {
			5: true, // start_clock_time_msec
		},
		reflect.TypeOf(&bspb.BatteryStats_System_BluetoothState{}): {
			0: true, // name
		},
		// It doesn't make sense to diff ChargeStep fields.
		reflect.TypeOf(&bspb.BatteryStats_System_DataConnection{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_KernelWakelock{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_PowerUseItem{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_PowerUseSummary{}): {
			0: true, // battery_capacity_mah
		},
		reflect.TypeOf(&bspb.BatteryStats_System_ScreenBrightness{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_SignalStrength{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_WakeupReason{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_WifiSignalStrength{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_WifiSupplicantState{}): {
			0: true, // name
		},
		reflect.TypeOf(&bspb.BatteryStats_System_WifiState{}): {
			0: true, // name
		},
	}
)

// PackageUIDGroupName returns the predefined group name for the given package name.
// It will return an empty string if there is no predefined group name.
func PackageUIDGroupName(pkg string) string {
	return packageNameToSharedUIDMap[pkg]
}

type stepData struct {
	timeMsec      float32
	level         float32
	displayState  *bspb.BatteryStats_System_DisplayState_State
	powerSaveMode *bspb.BatteryStats_System_PowerSaveMode_Mode
	idleMode      *bspb.BatteryStats_System_IdleMode_Mode
}

// WakelockInfo is a data structure used to sort wakelocks by time or count.
type WakelockInfo struct {
	Name          string
	UID           int32
	Duration      time.Duration
	MaxDuration   time.Duration
	TotalDuration time.Duration
	Count         float32
}

// byAbsTime sorts wakelock by absolute value of the time held.
type byAbsTime []*WakelockInfo

func (a byAbsTime) Len() int      { return len(a) }
func (a byAbsTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// sort by decreasing absolute value of time
func (a byAbsTime) Less(i, j int) bool {
	x, y := a[i].Duration, a[j].Duration
	return math.Abs(float64(x)) >= math.Abs(float64(y))
}

// SortByAbsTime ranks a slice of wakelocks by the absolute value of duration in desc order.
func SortByAbsTime(items []*WakelockInfo) {
	sort.Sort(byAbsTime(items))
}

// byTime sorts wakelock by the time held.
type byTime []*WakelockInfo

func (a byTime) Len() int      { return len(a) }
func (a byTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// sort by decreasing time order then increasing alphabetic order to break the tie.
func (a byTime) Less(i, j int) bool {
	if x, y := a[i].TotalDuration, a[j].TotalDuration; x != y {
		return x > y
	}
	if x, y := a[i].Duration, a[j].Duration; x != y {
		return x > y
	}
	return a[i].Name < a[j].Name
}

// byCount sorts wakelock by count.
type byCount []*WakelockInfo

func (a byCount) Len() int      { return len(a) }
func (a byCount) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// sort by decreasing time order then increasing alphabetic order to break the tie
func (a byCount) Less(i, j int) bool {
	if x, y := a[i].Count, a[j].Count; x != y {
		return x > y
	}
	return a[i].Name < a[j].Name
}

// SortByTime ranks a slice of wakelocks by duration.
func SortByTime(items []*WakelockInfo) {
	sort.Sort(byTime(items))
}

// SortByCount ranks a slice of wakelocks by count.
func SortByCount(items []*WakelockInfo) {
	sort.Sort(byCount(items))
}

// ParseBatteryStats parses the aggregated battery stats in checkin report
// according to frameworks/base/core/java/android/os/BatteryStats.java.
func ParseBatteryStats(pc checkinutil.Counter, cr *checkinutil.BatteryReport, pkgs []*usagepb.PackageInfo) (*bspb.BatteryStats, []string, []error) {
	// Support a single version and single aggregation type in a checkin report.
	var aggregationType bspb.BatteryStats_AggregationType
	var allAppComputedPowerMah float32
	apkSeen := make(map[apkID]bool)

	p := &bspb.BatteryStats{}
	p.System = &bspb.BatteryStats_System{}
	reportVersion := int32(-1) // Default to -1 so we can tell if the report version was parsed from the report.
	var warnings []string
	uids, errs := parsePackageManager(pc, cr.RawBatteryStats, pkgs)
	if len(errs) > 0 {
		return nil, warnings, errs
	}
	for _, r := range cr.RawBatteryStats {
		var rawUID int32
		var rawAggregationType, section string
		// The first element in r is '9', which used to be the report version but is now just there as a legacy field.
		remaining, err := parseSlice(pc, "All", r[1:], &rawUID, &rawAggregationType, &section)
		if err != nil {
			errs = append(errs, fmt.Errorf("error parsing entire line: %v", err))
			continue
		}

		if section == uidData {
			// uidData is parsed in the PackageManager function
			continue
		}

		if section == versionData { // first line may contain version information in new format
			if reportVersion != -1 {
				errs = append(errs, fmt.Errorf("multiple %s lines encountered", versionData))
			}
			// e.g., 9,0,i,vers,15,135,MMB08I,MMB08K
			if _, err := parseSlice(pc, versionData, remaining, &reportVersion); err != nil {
				errs = append(errs, fmt.Errorf("error parsing version data: %v", err))
			}
			continue
		}

		switch rawAggregationType {
		case sinceCharged:
			aggregationType = bspb.BatteryStats_SINCE_CHARGED
		case current:
			aggregationType = bspb.BatteryStats_CURRENT
		case unplugged:
			aggregationType = bspb.BatteryStats_SINCE_UNPLUGGED
		case info: // Not an aggregation type so nothing to do.
		default:
			errs = append(errs, fmt.Errorf("unsupported aggregation type %s", rawAggregationType))
			return nil, warnings, errs
		}

		uid := packageutils.AppID(rawUID)
		if uid == 0 {
			// Some lines that are parsed will provide a uid of 0, even though 0 is not the
			// correct uid for the relevant package.
			// See if we can find an application to claim this.
			pkg, err := packageutils.GuessPackage(strings.Join(remaining, ","), "", pkgs)
			if err != nil {
				errs = append(errs, err)
			}
			// Many applications will match with android, but we should already have the android
			// UID in the map, so ignore if a package matches with android.
			if pkg.GetPkgName() != "" && pkg.GetPkgName() != "android" {
				uid = packageutils.AppID(pkg.GetUid())
			}
		}
		stats, ok := uids[uid]
		if !ok {
			// Unexpected UID. Some packages are uploaded with a uid of 0 and so won't be found
			// until we come across a case like this. Try to determine package from list.
			// Passing an empty uid here because if we reach this case, then the uploaded package
			// in the list has a uid of 0.
			pkg, err := packageutils.GuessPackage(strings.Join(remaining, ","), "", pkgs)
			if err != nil {
				errs = append(errs, err)
			}
			if pkg.GetPkgName() != "" && pkg.GetPkgName() != "android" {
				stats = &bspb.BatteryStats_App{
					Name: pkg.PkgName,
					Uid:  proto.Int32(uid),
					Child: []*bspb.BatteryStats_App_Child{
						{
							Name:        pkg.PkgName,
							VersionCode: pkg.VersionCode,
							VersionName: pkg.VersionName,
						},
					},
				}
				uids[uid] = stats
			} else {
				if _, ok := KnownUIDs[uid]; !ok {
					// We've already gone through the entire package list, and it's not a UID
					// that we already know about, so log a warning.
					if packageutils.IsSandboxedProcess(uid) {
						warnings = append(warnings, fmt.Sprintf("found sandboxed uid for section %q", section))
					} else {
						warnings = append(warnings, fmt.Sprintf("found unexpected uid %d for section %q", uid, section))
					}
				}
				stats = &bspb.BatteryStats_App{Uid: proto.Int32(uid)}
				uids[uid] = stats
			}
		}
		system := p.GetSystem()
		// Parse csv lines according to
		// frameworks/base/core/java/android/os/BatteryStats.java.
		parsed, warn, csErrs := parseSection(pc, reportVersion, rawUID, section, remaining, stats, system, apkSeen, &allAppComputedPowerMah)
		e := false
		if e, warnings, errs = saveWarningsAndErrors(warnings, warn, errs, csErrs...); e {
			return nil, warnings, errs
		}
		if !parsed {
			warnings = append(warnings, fmt.Sprintf("unknown data category %s", section))
		}
	}
	if reportVersion == -1 {
		errs = append(errs, errors.New("no report version found"))
	} else if reportVersion < minParseReportVersion {
		errs = append(errs, fmt.Errorf("old report version %d", reportVersion))
	} else if reportVersion > maxParseReportVersion {
		warnings = append(warnings, fmt.Sprintf("newer report version %d found", reportVersion))
	}

	suidMap := make(map[int32]string)
	for _, pkg := range pkgs {
		if suid := pkg.GetSharedUserId(); suid != "" {
			if s, ok := suidMap[pkg.GetUid()]; ok && s != suid {
				errs = append(errs, fmt.Errorf("got UID %d with two different shared UID labels: %s and %s", pkg.GetUid(), s, suid))
				continue
			}
			suidMap[pkg.GetUid()] = suid
		}
	}
	processAppInfo(uids, suidMap)

	// Copy uids to p.App.
	for _, app := range uids {
		aggregateAppApk(app)
		p.App = append(p.App, app)
	}
	// Copy to p.System.
	p.System.PowerUseItem = append(p.System.PowerUseItem,
		&bspb.BatteryStats_System_PowerUseItem{
			Name:             bspb.BatteryStats_System_PowerUseItem_APP.Enum(),
			ComputedPowerMah: proto.Float32(allAppComputedPowerMah),
		})
	if m := p.System.GetMisc(); m != nil {
		// Screen off time is calculated by subtracting screen on time from total battery real time.
		diff := p.System.GetBattery().GetBatteryRealtimeMsec() - m.GetScreenOnTimeMsec()
		m.ScreenOffTimeMsec = proto.Float32(diff)
		if diff < 0 {
			errs = append(errs, fmt.Errorf("negative screen off time"))
		}
	}
	p.ReportVersion = proto.Int32(reportVersion)
	p.AggregationType = aggregationType.Enum()
	p.StartTimeUsec = proto.Int64(p.System.GetBattery().GetStartClockTimeMsec() * 1000)
	p.EndTimeUsec = proto.Int64(cr.TimeUsec)
	gmsPkg, err := packageutils.GuessPackage("com.google.android.gms", "", pkgs)
	if err != nil {
		errs = append(errs, err)
	}
	if gmsPkg != nil {
		p.GmsVersion = gmsPkg.VersionCode
	}

	p.Build = build.Build(cr.BuildID)
	if p.GetBuild().GetType() == "user" {
		for _, tag := range p.GetBuild().GetTags() {
			if tag == "release-keys" {
				p.IsUserRelease = proto.Bool(true)
				break
			}
		}
	}
	p.DeviceGroup = cr.DeviceGroup
	p.CheckinRule = cr.CheckinRule
	p.Radio = proto.String(cr.Radio)
	p.SdkVersion = proto.Int32(cr.SDKVersion)
	p.Bootloader = proto.String(cr.Bootloader)
	if cr.CellOperator != "" {
		p.Carrier = proto.String(cr.CellOperator + "/" + cr.CountryCode)
	}
	p.CountryCode = proto.String(cr.CountryCode)
	p.TimeZone = proto.String(cr.TimeZone)

	postProcessBatteryStats(p)

	return p, warnings, errs
}

// CreateBatteryReport creates a checkinutil.BatteryReport from the given
// sessionpb.Checkin.
func CreateBatteryReport(cp *sessionpb.Checkin) *checkinutil.BatteryReport {
	return &checkinutil.BatteryReport{
		TimeUsec:        cp.GetBucketSnapshotMsec() * 1000,
		TimeZone:        cp.GetSystemInfo().GetTimeZone(),
		AndroidID:       cp.GetAndroidId(),
		DeviceGroup:     cp.Groups,
		BuildID:         cp.GetBuildFingerprint(),
		Radio:           cp.GetSystemInfo().GetBasebandRadio(),
		Bootloader:      cp.GetSystemInfo().GetBootloader(),
		SDKVersion:      cp.GetSystemInfo().GetSdkVersion(),
		CellOperator:    cp.GetSystemInfo().GetNetworkOperator(),
		CountryCode:     cp.GetSystemInfo().GetCountryCode(),
		RawBatteryStats: extractCSV(cp.GetCheckin()),
	}
}

// saveWarningsAndErrors appends new errors (ne) and new warnings (nw) to existing slices
// and returns true if new errors were added
func saveWarningsAndErrors(warnings []string, nw string, errors []error, ne ...error) (bool, []string, []error) {
	newErr := false
	for _, e := range ne {
		if e != nil {
			errors = append(errors, e)
			newErr = true
		}
	}
	if len(nw) > 0 {
		warnings = append(warnings, nw)
	}
	return newErr, warnings, errors
}

// extractCSV extracts the checkin package information and last charged stats from the given input.
func extractCSV(input string) [][]string {
	// bs contains since last charged stats, separated by line and comma
	var bs [][]string
	// we only use since charged data (ignore unplugged data)
	for _, arr := range checkinutil.ParseCSV(input) {
		if len(arr) < minNumFields {
			continue
		}
		switch arr[2] {
		case info, sinceCharged:
			bs = append(bs, arr)
		}
	}
	return bs
}

// parseSection parses known checkin sections and returns true if the section was parsed.
// The app and system protos are directly modified.
func parseSection(c checkinutil.Counter, reportVersion, rawUID int32, section string, record []string, app *bspb.BatteryStats_App, system *bspb.BatteryStats_System, apkSeen map[apkID]bool, allAppComputedPowerMah *float32) (bool, string, []error) {
	switch section {
	case apkData:
		warn, errs := parseChildApk(c, record, app, apkSeen, rawUID)
		return true, warn, errs
	case audioData:
		if app.GetAudio() == nil {
			app.Audio = &bspb.BatteryStats_App_Audio{}
		}
		warn, errs := parseAndAccumulate(audioData, record, app.GetAudio())
		return true, warn, errs
	case batteryData:
		warn, errs := SystemBattery(c, record, system)
		return true, warn, errs
	case batteryDischargeData:
		warn, errs := parseSystemBatteryDischarge(c, record, system)
		return true, warn, errs
	case batteryLevelData:
		warn, errs := parseSystemBatteryLevel(c, record, system)
		return true, warn, errs
	case bluetoothControllerData:
		if app.GetBluetoothController() == nil {
			d, err := parseControllerData(c, bluetoothControllerData, record)
			if err != nil {
				return true, "", []error{err}
			}

			app.BluetoothController = d
			return true, "", nil
		}

		if err := parseAndAccumulateControllerData(c, bluetoothControllerData, record, app.BluetoothController); err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case bluetoothMiscData:
		if app.GetBluetoothMisc() == nil {
			app.BluetoothMisc = &bspb.BatteryStats_App_BluetoothMisc{}
		}
		warn, errs := parseAndAccumulate(bluetoothMiscData, record, app.GetBluetoothMisc())
		return true, warn, errs
	case cameraData:
		if app.GetCamera() == nil {
			app.Camera = &bspb.BatteryStats_App_Camera{}
		}
		warn, errs := parseAndAccumulate(cameraData, record, app.GetCamera())
		return true, warn, errs
	case chargeStepData:
		data, warn, err := parseStepData(record)
		if err != nil {
			return true, warn, []error{err}
		}

		system.ChargeStep = append(system.ChargeStep, &bspb.BatteryStats_System_ChargeStep{
			TimeMsec:      proto.Float32(data.timeMsec),
			Level:         proto.Float32(data.level),
			DisplayState:  data.displayState,
			PowerSaveMode: data.powerSaveMode,
			IdleMode:      data.idleMode,
		})
		return true, warn, nil
	case chargeTimeRemainData: // Current format: 9,0,i,ctr,18147528000
		if system.ChargeTimeRemaining != nil {
			c.Count("error-charge-time-remaining-exist", 1)
			return true, "", []error{errors.New("charge time remaining field already exists")}
		}
		ctr := &bspb.BatteryStats_System_ChargeTimeRemaining{}
		warn, errs := parseLine(chargeTimeRemainData, record, ctr)
		if len(errs) == 0 {
			system.ChargeTimeRemaining = ctr
		}
		return true, warn, errs
	case cpuData:
		if app.GetCpu() == nil {
			app.Cpu = &bspb.BatteryStats_App_Cpu{}
		}
		warn, errs := parseAndAccumulate(cpuData, record, app.GetCpu())
		return true, warn, errs
	case dischargeStepData:
		data, warn, err := parseStepData(record)
		if err != nil {
			return true, warn, []error{err}
		}

		system.DischargeStep = append(system.DischargeStep, &bspb.BatteryStats_System_DischargeStep{
			TimeMsec:      proto.Float32(data.timeMsec),
			Level:         proto.Float32(data.level),
			DisplayState:  data.displayState,
			PowerSaveMode: data.powerSaveMode,
			IdleMode:      data.idleMode,
		})
		return true, warn, nil
	case dischargeTimeRemainData: // Current format: 9,0,i,dtr,18147528000
		if system.DischargeTimeRemaining != nil {
			c.Count("error-discharge-time-remaining-exist", 1)
			return true, "", []error{errors.New("discharge time remaining field already exists")}
		}
		dtr := &bspb.BatteryStats_System_DischargeTimeRemaining{}
		warn, errs := parseLine(dischargeTimeRemainData, record, dtr)
		if len(errs) == 0 {
			system.DischargeTimeRemaining = dtr
		}
		return true, warn, errs
	case flashlightData:
		if app.GetFlashlight() == nil {
			app.Flashlight = &bspb.BatteryStats_App_Flashlight{}
		}
		warn, errs := parseAndAccumulate(flashlightData, record, app.GetFlashlight())
		return true, warn, errs
	case foregroundData:
		if app.GetForeground() == nil {
			app.Foreground = &bspb.BatteryStats_App_Foreground{}
		}
		warn, errs := parseAndAccumulate(foregroundData, record, app.GetForeground())
		return true, warn, errs
	case globalBluetoothControllerData:
		if reportVersion < 17 {
			warn, errs := parseGlobalBluetooth(record, system)
			return true, warn, errs
		}

		d, err := parseControllerData(c, globalBluetoothControllerData, record)
		if err != nil {
			return true, "", []error{err}
		}

		system.GlobalBluetoothController = d
		return true, "", nil
	case globalModemControllerData:
		d, err := parseControllerData(c, globalModemControllerData, record)
		if err != nil {
			return true, "", []error{err}
		}

		system.GlobalModemController = d
		return true, "", nil
	case globalNetworkData:
		warn, errs := parseGlobalNetwork(c, record, system)
		return true, warn, errs
	case globalWifiControllerData:
		d, err := parseControllerData(c, globalWifiControllerData, record)
		if err != nil {
			return true, "", []error{err}
		}

		system.GlobalWifiController = d
		return true, "", nil
	case globalWifiData:
		warn, errs := parseGlobalWifi(record, system)
		return true, warn, errs
	case jobData:
		warn, errs := parseAppScheduledJob(record, app)
		return true, warn, errs
	case kernelWakelockData:
		warn, errs := parseSystemKernelWakelock(record, system)
		return true, warn, errs
	case miscData:
		warn, errs := parseSystemMisc(c, reportVersion, record, system)
		return true, warn, errs
	case modemControllerData:
		if app.GetModemController() == nil {
			d, err := parseControllerData(c, modemControllerData, record)
			if err != nil {
				return true, "", []error{err}
			}

			app.ModemController = d
			return true, "", nil
		}

		if err := parseAndAccumulateControllerData(c, modemControllerData, record, app.ModemController); err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case networkData:
		warn, errs := parseAppNetwork(record, app)
		return true, warn, errs
	case powerUseItemData:
		warn, err := parseAppSystemPowerUseItem(c, record, app, system, allAppComputedPowerMah)
		if err != nil {
			return true, warn, []error{err}
		}
		return true, warn, nil
	case powerUseSummaryData:
		warn, errs := parseSystemPowerUseSummary(c, record, system)
		return true, warn, errs
	case processData:
		warn, errs := parseAppProcess(record, app)
		return true, warn, errs
	case screenBrightnessData:
		err := parseSystemScreenBrightness(c, record, system)
		if err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case sensorData:
		warn, errs := parseAppSensor(record, app)
		return true, warn, errs
	case signalScanningTimeData:
		warn, errs := parseSystemSignalScanningTime(c, record, system)
		return true, warn, errs
	case stateTimeData:
		warn, errs := parseAppStateTime(c, record, reportVersion, app)
		return true, warn, errs
	case syncData:
		warn, errs := parseAppSync(record, app)
		return true, warn, errs
	case userActivityData:
		if packageutils.AppID(rawUID) != 0 {
			warn, err := parseAppUserActivity(record, app)
			if err != nil {
				return true, warn, []error{err}
			}
			return true, warn, nil
		}
		return true, "", nil
	case vibratorData:
		if app.GetVibrator() == nil {
			app.Vibrator = &bspb.BatteryStats_App_Vibrator{}
		}
		warn, errs := parseAndAccumulate(vibratorData, record, app.GetVibrator())
		return true, warn, errs
	case videoData:
		if app.GetVideo() == nil {
			app.Video = &bspb.BatteryStats_App_Video{}
		}
		warn, errs := parseAndAccumulate(videoData, record, app.GetVideo())
		return true, warn, errs
	case wakelockData:
		warn, err := parseAppWakelock(c, reportVersion, record, app)
		if err != nil {
			return true, warn, []error{err}
		}
		return true, warn, nil
	case wakeupAlarmData:
		warn, errs := parseAppWakeupAlarm(record, app)
		return true, warn, errs
	case wakeupReasonData:
		warn, errs := parseSystemWakeupReason(record, system)
		return true, warn, errs
	case wifiControllerData:
		if app.GetWifiController() == nil {
			d, err := parseControllerData(c, wifiControllerData, record)
			if err != nil {
				return true, "", []error{err}
			}

			app.WifiController = d
			return true, "", nil
		}

		err := parseAndAccumulateControllerData(c, wifiControllerData, record, app.WifiController)
		if err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case wifiData:
		warn, errs := parseAppWifi(record, app)
		return true, warn, errs
	case signalStrengthTimeData, signalStrengthCountData, dataConnectionTimeData, dataConnectionCountData, wifiStateTimeData, wifiStateCountData, bluetoothStateTimeData, bluetoothStateCountData, wifiSupplStateTimeData, wifiSupplStateCountData, wifiSignalStrengthTimeData, wifiSignalStrengthCountData:
		warn, err := parseSystemTimeCountPair(c, section, record, system)
		if err != nil {
			return true, warn, []error{err}
		}
		return true, warn, nil
	default:
		return false, "", nil
	}
}

// postProcessBatteryStats handles processing and data population of special fields.
func postProcessBatteryStats(bs *bspb.BatteryStats) {
	if bs == nil {
		return
	}

	postProcessSystem(bs.System, bs.GetReportVersion())

	for _, a := range bs.GetApp() {
		postProcessApp(a)
	}
}

// postProcessSystem handles processing and data population of special fields in the System proto.
func postProcessSystem(sys *bspb.BatteryStats_System, reportVersion int32) {
	if sys == nil {
		return
	}
	// Charge and discharge step info is printed out (and thus read) in reverse order
	// (low to high for discharge and high to low for charge) so reverse the order to make more sense.
	if cs := sys.GetChargeStep(); cs != nil {
		for i := 0; i < len(cs)/2; i++ {
			j := len(cs) - i - 1
			cs[i], cs[j] = cs[j], cs[i]
		}
	}
	if ds := sys.GetDischargeStep(); ds != nil {
		for i := 0; i < len(ds)/2; i++ {
			j := len(ds) - i - 1
			ds[i], ds[j] = ds[j], ds[i]
		}
	}

	// Convenience functions to avoid repeating code.
	makeGWC := func() {
		if gw := sys.GlobalWifi; gw != nil {
			sys.GlobalWifiController = &bspb.BatteryStats_ControllerActivity{
				IdleTimeMsec: proto.Int64(int64(gw.GetWifiIdleTimeMsec())),
				RxTimeMsec:   proto.Int64(int64(gw.GetWifiRxTimeMsec())),
				PowerMah:     proto.Int64(int64(gw.GetWifiPowerMah())),
				Tx: []*bspb.BatteryStats_ControllerActivity_TxLevel{
					{
						Level:    proto.Int32(0),
						TimeMsec: proto.Int64(int64(gw.GetWifiTxTimeMsec())),
					},
				},
			}
		}
	}
	makeGB := func() {
		if gbc := sys.GlobalBluetoothController; gbc != nil {
			gb := &bspb.BatteryStats_System_GlobalBluetooth{}
			gb.BluetoothIdleTimeMsec = proto.Float32(float32(gbc.GetIdleTimeMsec()))
			gb.BluetoothRxTimeMsec = proto.Float32(float32(gbc.GetRxTimeMsec()))
			gb.BluetoothPowerMah = proto.Float32(float32(gbc.GetPowerMah()))
			var t int64
			for _, tx := range gbc.Tx {
				t += tx.GetTimeMsec()
			}
			gb.BluetoothTxTimeMsec = proto.Float32(float32(t))
			sys.GlobalBluetooth = gb
		}
	}

	// This is all in one function to provide context and because there wasn't
	// a clear line to allow breaking things into smaller functions.
	// TODO: put things into smaller functions if there are clearer lines.
	if reportVersion >= 17 {
		if gwc := sys.GlobalWifiController; reportVersion >= 18 || gwc != nil {
			// This report version is post-controller activity change.
			// Populate incorrect/missing data in old fields.
			if gwc != nil {
				gw := sys.GlobalWifi
				if gw == nil {
					gw = &bspb.BatteryStats_System_GlobalWifi{}
					sys.GlobalWifi = gw
				}
				gw.WifiIdleTimeMsec = proto.Float32(float32(gwc.GetIdleTimeMsec()))
				gw.WifiRxTimeMsec = proto.Float32(float32(gwc.GetRxTimeMsec()))
				gw.WifiPowerMah = proto.Float32(float32(gwc.GetPowerMah()))
				var t int64
				for _, tx := range sys.GlobalWifiController.Tx {
					t += tx.GetTimeMsec()
				}
				gw.WifiTxTimeMsec = proto.Float32(float32(t))
			}

			// Going to use the presence of GlobalWifiController to indicate that the gble data
			// is controller activity data. Not perfect, but there's no guarantee in version 17
			// about what kind it is, and the power and tx_time fields were swapped, so we must
			// [try to] fix them.
			makeGB()
		} else {
			makeGWC()

			if gbc := sys.GlobalBluetoothController; gbc != nil {
				if len(gbc.Tx) == 1 {
					// GlobalWifiController is nil and there was only one transmit level detected.
					// Assume it's the old gble format. Must swap the power and tx_time data.
					cPwr, cTx := gbc.GetTx()[0].TimeMsec, gbc.PowerMah
					gbc.PowerMah, gbc.GetTx()[0].TimeMsec = cPwr, cTx
				}
				makeGB()
			}
		}
	}
	if reportVersion < 17 {
		// Copy values into newer protos so that analyses can rely more on the newer protos.
		if gb := sys.GlobalBluetooth; gb != nil {
			sys.GlobalBluetoothController = &bspb.BatteryStats_ControllerActivity{
				IdleTimeMsec: proto.Int64(int64(gb.GetBluetoothIdleTimeMsec())),
				RxTimeMsec:   proto.Int64(int64(gb.GetBluetoothRxTimeMsec())),
				PowerMah:     proto.Int64(int64(gb.GetBluetoothPowerMah())),
				Tx: []*bspb.BatteryStats_ControllerActivity_TxLevel{
					{
						Level:    proto.Int32(0),
						TimeMsec: proto.Int64(int64(gb.GetBluetoothTxTimeMsec())),
					},
				},
			}
		}

		makeGWC()
	}
	if m := sys.Misc; m != nil {
		if reportVersion >= 14 {
			// These won't have been populated through regular parsing for version 14+ so it's
			// safe to overwrite here.
			if gw := sys.GlobalWifi; gw != nil {
				m.WifiOnTimeMsec = gw.WifiOnTimeMsec
				m.WifiRunningTimeMsec = gw.WifiRunningTimeMsec
			}
			if gn := sys.GlobalNetwork; gn != nil {
				m.MobileBytesRx = gn.MobileBytesRx
				m.MobileBytesTx = gn.MobileBytesTx
				m.WifiBytesRx = gn.WifiBytesRx
				m.WifiBytesTx = gn.WifiBytesTx
			}
		} else {
			// sys.GlobalWifi won't have been populated through regular parsing for version 13-
			// so it's safe to overwrite here.
			sys.GlobalWifi = &bspb.BatteryStats_System_GlobalWifi{
				WifiOnTimeMsec:      m.WifiOnTimeMsec,
				WifiRunningTimeMsec: m.WifiRunningTimeMsec,
			}
			// Mobile and wifi bytes were being printed in both misc and GlobalNetwork lines for
			// report versions 7-13 so no need to re-assign sys.GlobalNetwork.WifiBytes* and
			// sys.GlobalNetwork.MobileBytes* here.
		}
	}
}

// postProcessApp handles processing and data population of special fields in an App proto.
func postProcessApp(app *bspb.BatteryStats_App) {
	if app == nil {
		return
	}

	// If WifiController data exists, we can use it to populate Wifi data.
	if wc := app.WifiController; wc != nil {
		if app.Wifi == nil {
			app.Wifi = &bspb.BatteryStats_App_Wifi{}
		}
		w := app.Wifi
		// Populate old fields. These fields will be 0 if WifiController data exists,
		// so it's okay to overwrite.
		w.IdleTimeMsec = proto.Float32(float32(wc.GetIdleTimeMsec()))
		w.RxTimeMsec = proto.Float32(float32(wc.GetRxTimeMsec()))
		var t int64
		for _, tx := range wc.Tx {
			t += tx.GetTimeMsec()
		}
		w.TxTimeMsec = proto.Float32(float32(t))
	} else if w := app.Wifi; w != nil {
		// WifiController data doesn't exist, but Wifi does. Use it to populate WifiController data.
		app.WifiController = &bspb.BatteryStats_ControllerActivity{
			IdleTimeMsec: proto.Int64(int64(w.GetIdleTimeMsec())),
			RxTimeMsec:   proto.Int64(int64(w.GetRxTimeMsec())),
			Tx: []*bspb.BatteryStats_ControllerActivity_TxLevel{
				{
					Level:    proto.Int32(0),
					TimeMsec: proto.Int64(int64(w.GetTxTimeMsec())),
				},
			},
		}
	}
}

// GroupName returns a predefined UID group name for the given sharedUserID label.
// If there is no predefined name, then an empty string is returned.
func GroupName(sharedUIDLabel string) string {
	return sharedUIDLabelMap[sharedUIDLabel]
}

// parseStepData parses sliced lines that were orginally in the form 9,0,i,csd,65975,40,sd,p-
// Use this to parse lines for CHARGE_STEP_DATA (csd) and DISCHARGE_STEP_DATA (dsd).
func parseStepData(r []string) (*stepData, string, error) {
	var err error
	var warn string
	var tm, l float32
	var ds bspb.BatteryStats_System_DisplayState_State
	var psm bspb.BatteryStats_System_PowerSaveMode_Mode
	var im bspb.BatteryStats_System_IdleMode_Mode

	if len(r) > 0 {
		tm, err = parseFloat32(r[0])
		if err != nil {
			return nil, warn, err
		}
	}

	if len(r) > 1 {
		// A ? indicates a bug/error with the display state in the log and so we should treat it as such.
		// The ? was intended for index 2, but was in index 1 due to a (now fixed) bug.
		if r[1] == "?" {
			return nil, warn, errors.New("discovered ? for display state")
		}
		l, err = parseFloat32(r[1])
		if err != nil {
			return nil, warn, err
		}
	}

	ds = bspb.BatteryStats_System_DisplayState_MIXED
	if len(r) > 2 {
		switch r[2] {
		case "s+":
			ds = bspb.BatteryStats_System_DisplayState_ON
		case "s-":
			ds = bspb.BatteryStats_System_DisplayState_OFF
		case "sd":
			ds = bspb.BatteryStats_System_DisplayState_DOZE
		case "sds":
			ds = bspb.BatteryStats_System_DisplayState_DOZE_SUSPEND
		case "": // Empty strings are valid.
			ds = bspb.BatteryStats_System_DisplayState_MIXED
		case "?": // A ? indicates a bug/error in the log and so we should treat it as such.
			return nil, warn, errors.New("discovered ? when parsing display state")
		default:
			warn = fmt.Sprintf("unknown display state: %s", r[2])
		}
	}

	psm = bspb.BatteryStats_System_PowerSaveMode_MIXED
	im = bspb.BatteryStats_System_IdleMode_NO_DATA
	if len(r) > 3 {
		switch r[3] {
		case "p+":
			psm = bspb.BatteryStats_System_PowerSaveMode_ON
		case "p-":
			psm = bspb.BatteryStats_System_PowerSaveMode_OFF
		case "": // Empty strings are valid.
			psm = bspb.BatteryStats_System_PowerSaveMode_MIXED
		// i+ and i- were in the 4th slot due to a (now fixed) bug.
		case "i+":
			im = bspb.BatteryStats_System_IdleMode_ON
		case "i-":
			im = bspb.BatteryStats_System_IdleMode_OFF
		default:
			warn = fmt.Sprintf("unknown power save mode: %s", r[3])
		}
	}

	if len(r) > 4 {
		switch r[4] {
		case "i+":
			im = bspb.BatteryStats_System_IdleMode_ON
		case "i-":
			im = bspb.BatteryStats_System_IdleMode_OFF
		case "": // Empty strings are valid.
			im = bspb.BatteryStats_System_IdleMode_MIXED
		default:
			warn = fmt.Sprintf("unknown idle mode: %s", r[4])
		}
	}

	return &stepData{
		timeMsec:      tm,
		level:         l,
		displayState:  &ds,
		powerSaveMode: &psm,
		idleMode:      &im,
	}, warn, nil
}

// processAppInfo tries to determine the best name and version_code to assign
// to the BatteryStats_App element of each shared uid.
func processAppInfo(uids map[int32]*bspb.BatteryStats_App, suidMap map[int32]string) []error {
	var errs []error

	for uid, app := range uids {
		known := false
		var headChild *bspb.BatteryStats_App_Child
		var childNames []string

		suid := suidMap[uid]
		if l := GroupName(suid); l != "" {
			app.Name = proto.String(l)
			// Only mark known if the shared UID label has a predefined name.
			// We will use the shared UID label itself later on if there are
			// multiple packages in this proto.
			known = true
		}

		for _, child := range app.Child {
			childNames = append(childNames, child.GetName())

			// Check to see if it's any of the other shared UIDs.
			if val, ok := packageNameToSharedUIDMap[child.GetName()]; !known && ok {
				app.Name = proto.String(val)
				known = true
			} else if known && ok && app.GetName() != val {
				errs = append(errs, fmt.Errorf("package groups not matched properly"))
			}
		}

		app.Uid = proto.Int32(uid)
		if name, ok := KnownUIDs[uid]; ok {
			app.Name = proto.String(name)
			known = true
		}

		if len(app.Child) == 1 && headChild == nil {
			headChild = app.GetChild()[0]
		}

		if headChild != nil {
			app.VersionCode = headChild.VersionCode
			app.VersionName = headChild.VersionName
		}

		// The children aren't part of any known shared UIDs.
		// Creating a name based on the concatenation of children's names, as defined in ../batterystats/parse.go
		if !known {
			if suid != "" && len(childNames) != 1 {
				app.Name = proto.String(fmt.Sprintf("SharedUserID(%s)", suid))
				continue
			}
			prefix := ""
			if len(childNames) == 0 && len(app.GetProcess()) > 0 {
				// No children...process names may include clues as to what this UID was for.
				for _, p := range app.GetProcess() {
					childNames = append(childNames, p.GetName())
				}
				prefix = "process:"
			}
			sort.Strings(childNames) // Needed for consistent ordering
			app.Name = proto.String(prefix + strings.Join(childNames, "|"))
		}
	}

	return errs
}

// parsePackageManager parses the uid section of the checkin report.
// It builds a map from UID to the relevant BatteryStats_App proto.
// All packages that share the same UID are listed in the proto's list
// of children. The rest of the proto is filled in later.
func parsePackageManager(pc checkinutil.Counter, bs [][]string, pkgs []*usagepb.PackageInfo) (map[int32]*bspb.BatteryStats_App, []error) {
	// Use a map to ensure we don't double count packages.
	apps := make(map[string]*usagepb.PackageInfo)
	for _, p := range pkgs {
		apps[p.GetPkgName()] = p
	}

	uids := make(map[int32]*bspb.BatteryStats_App)
	var errs []error

	for _, r := range bs {
		// Expect inputs like "8,0,i,uid,1000,com.android.settings".
		if r[3] != uidData {
			continue
		}
		var uid int32
		var name string
		if _, err := parseSlice(pc, r[3], r[4:], &uid, &name); err != nil {
			errs = append(errs, fmt.Errorf("error parsing PackageManager: %v", err))
			continue
		}
		uid = packageutils.AppID(uid)
		if _, known := apps[name]; !known {
			apps[name] = &usagepb.PackageInfo{
				PkgName: proto.String(name),
				Uid:     proto.Int32(uid),
			}
		} else if packageutils.AppID(apps[name].GetUid()) == 0 {
			// It looks like a lot of packages have a uid of 0 coming from the device.
			// TODO: figure out why we're getting UIDs of 0 on the device side.
			apps[name].Uid = proto.Int32(uid)
		}
	}

	// We've gone through both sources of package/uid info; now we can create the App and Child elements.
	for _, p := range apps {
		uid := packageutils.AppID(p.GetUid())
		if uid == 0 {
			// Skip packages uploaded with a UID of 0 (this would be the case for old builds).
			continue
		}
		stats := uids[uid]
		if stats == nil {
			stats = &bspb.BatteryStats_App{
				Uid: proto.Int32(uid),
			}
			uids[uid] = stats
		}
		stats.Child = append(stats.Child,
			&bspb.BatteryStats_App_Child{
				Name:        p.PkgName,
				VersionCode: p.VersionCode,
				VersionName: p.VersionName,
			})
	}

	return uids, errs
}

// apkID identifies App.Apk whose App can have multiple UIDs due to multi-user support.
type apkID struct {
	// UID specified in raw BatteryStats data, not the result of modulo operation (% 100000) for
	// multi-user (e.g., 1110066 instead of the actual UID 10066)
	RawUID  int32
	PkgName string
}

// parseChildApk parses "apk" (APK_DATA) into the relevant child proto in app.
//
// If there exists a service entry with the same name, newly found values (e.g. StartTimeMsec) from
// parsing will be added to their corresponding fields in that entry. Otherwise, a new entry will be created.
//
// apkSeen maintains apkIDs whose Apk.Wakeups were correctly handled already.
// rawUID is the raw UID as explained in apkID.
//
// format: 8,1000,l,apk,5719,android,android.hardware.location.GeofenceHardwareService,0,0,2
// wakeup alarms, Apk name, service name, time spent started, start count, launch count
func parseChildApk(pc checkinutil.Counter, record []string, app *bspb.BatteryStats_App, apkSeen map[apkID]bool, rawUID int32) (string, []error) {
	var name string
	var wakeups float32
	// "wakeups" and "name" are shared across services of the same child apk.
	record, err := parseSlice(pc, apkData, record, &wakeups, &name)
	if err != nil {
		return "", []error{err}
	}
	var c *bspb.BatteryStats_App_Child
	// pkg name should have been added when parsing package manager("uid").
	for _, child := range app.GetChild() {
		if child.GetName() == name {
			c = child
			break
		}
	}
	if c == nil {
		if app.HeadChild != nil && app.GetHeadChild().GetName() == name {
			c = app.HeadChild
		} else {
			c = &bspb.BatteryStats_App_Child{Name: proto.String(name)}
			app.Child = append(app.Child, c)
		}
	}

	if id := (apkID{rawUID, name}); !apkSeen[id] { // We need to process "wakeups" only once per rawUID/pkgName pair.
		apk := c.GetApk()
		if apk == nil {
			apk = &bspb.BatteryStats_App_Apk{}
			c.Apk = apk
		}
		apk.Wakeups = proto.Float32(apk.GetWakeups() + wakeups)
		apkSeen[id] = true
	}

	s := &bspb.BatteryStats_App_Apk_Service{}
	warn, plErrs := parseLine(apkData, record, s)
	if len(plErrs) > 0 {
		return warn, plErrs
	}

	// Check to see if we've already encountered a service of the same name for this particular child.
	// If so, then increment the parsed values.
	for _, s1 := range c.Apk.Service {
		if s1.GetName() == s.GetName() {
			if err := accumulate(s1, s); err != nil {
				return warn, []error{fmt.Errorf("problem accumulating service: %v", err)}
			}
			return warn, nil
		}
	}

	// s wasn't found in the child's list of services so add it as a new one.
	c.Apk.Service = append(c.Apk.Service, s)
	return warn, nil
}

// aggregateAppApk iterates through the app's child list and aggregates the apk data of each child.
// The result is saved into app.Apk.
func aggregateAppApk(app *bspb.BatteryStats_App) {
	apk := &bspb.BatteryStats_App_Apk{}
	services := make(map[string]*bspb.BatteryStats_App_Apk_Service)
	headCounted := app.HeadChild == nil
	for _, child := range app.GetChild() {
		if child.Apk != nil {
			if child.GetName() == app.GetHeadChild().GetName() {
				headCounted = true
			}
			apk.Wakeups = proto.Float32(apk.GetWakeups() + child.GetApk().GetWakeups())
			for _, service := range child.GetApk().GetService() {
				if appServ, ok := services[service.GetName()]; ok {
					appServ.StartTimeMsec = proto.Float32(appServ.GetStartTimeMsec() + service.GetStartTimeMsec())
					appServ.Starts = proto.Float32(appServ.GetStarts() + service.GetStarts())
					appServ.Launches = proto.Float32(appServ.GetLaunches() + service.GetLaunches())
				} else {
					services[service.GetName()] = service
				}
			}
		}
	}
	h := app.GetHeadChild()
	if !headCounted && h.Apk != nil {
		apk.Wakeups = proto.Float32(apk.GetWakeups() + h.GetApk().GetWakeups())
		for _, service := range h.GetApk().GetService() {
			if appServ, ok := services[service.GetName()]; ok {
				appServ.StartTimeMsec = proto.Float32(appServ.GetStartTimeMsec() + service.GetStartTimeMsec())
				appServ.Starts = proto.Float32(appServ.GetStarts() + service.GetStarts())
				appServ.Launches = proto.Float32(appServ.GetLaunches() + service.GetLaunches())
			} else {
				services[service.GetName()] = service
			}
		}
	}

	if len(services) == 0 {
		// There were no apk lines for this UID.
		return
	}

	for _, s := range services {
		apk.Service = append(apk.Service, s)
	}

	app.Apk = apk
}

// parseAppProcess parses "pr" (PROCESS_DATA) in app.
//
// If there exists a process with the same name, newly found values (e.g. UserTimeMsec) from
// parsing will be added to their corresponding fields in that process. Otherwise, a new
// process will be created.
//
// record holds content from BatteryStats's processData.
//   e.g., com.google.android.music:main,0,10,0,0
func parseAppProcess(record []string, app *bspb.BatteryStats_App) (string, []error) {
	p := &bspb.BatteryStats_App_Process{}
	warn, errs := parseLine(processData, record, p)
	if len(errs) > 0 {
		return warn, errs
	}

	for _, p1 := range app.Process {
		if p1.GetName() == p.GetName() {
			if err := accumulate(p1, p); err != nil {
				return warn, []error{fmt.Errorf("problem accumulating process: %v", err)}
			}
			return warn, nil
		}
	}

	// p wasn't found in app's list of processes so add it as a new one.
	app.Process = append(app.Process, p)
	return warn, nil
}

// parseAppUserActivity parses "ua" (USER_ACTIVITY_TYPES) in App.
// format: 8,1000,l,ua,2,0,0[,3]
// "other", "button", "touch", ["accessibility"] activity counts
func parseAppUserActivity(record []string, app *bspb.BatteryStats_App) (string, error) {
	m := bspb.BatteryStats_App_UserActivity_Name_name
	warn := ""
	if n, b := len(record), len(m); n > b {
		warn = fmt.Sprintf("user activity now has %d additional fields", n-b)
	}
	for i, rawCount := range record {
		c, err := parseFloat32(rawCount)
		if err != nil {
			return warn, err
		}
		if len(app.UserActivity) > i {
			// Append the count to an existing message if possible.
			// The order is the same for all ua lines, so the indices should be the same.
			app.GetUserActivity()[i].Count = proto.Float32(app.GetUserActivity()[i].GetCount() + c)
			continue
		}
		app.UserActivity = append(app.UserActivity,
			&bspb.BatteryStats_App_UserActivity{
				Name:  bspb.BatteryStats_App_UserActivity_Name(i).Enum(),
				Count: proto.Float32(c),
			})
	}
	return warn, nil
}

// parseAppSensor parses "sr" (SENSOR_DATA) in App.
//
// If there exists a sensor entry with the same number, newly found values (e.g. TotalTimeMsec) from
// parsing will be added to their corresponding fields in that entry. Otherwise, a new entry will be created.
//
// record holds content from BatteryStats's sensorData.
//   e.g., 5,45859,3,2,73820,53820
func parseAppSensor(record []string, app *bspb.BatteryStats_App) (string, []error) {
	s := &bspb.BatteryStats_App_Sensor{}
	warn, errs := parseLine(sensorData, record, s)
	if len(errs) > 0 {
		return warn, errs
	}

	for _, s1 := range app.Sensor {
		if s1.GetNumber() == s.GetNumber() {
			accumulate(s1, s)
			return warn, nil
		}
	}

	// s wasn't found in app's list of sensor usage so add it as a new one.
	app.Sensor = append(app.Sensor, s)
	return warn, nil
}

// parseSystemPowerUseSummary parses "pws" (POWER_USE_SUMMARY_DATA) in System.
//
// record holds content from BatteryStats's powerUseSummaryData.
//   e.g., 2100,76.7,84.0,105
func parseSystemPowerUseSummary(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetPowerUseSummary() != nil {
		pc.Count("error-parse-system-power-use-summary-exist", 1)
		return "", []error{errors.New("power use summary field already exists")}
	}
	p := &bspb.BatteryStats_System_PowerUseSummary{}
	warn, errs := parseLine(powerUseSummaryData, record, p)
	if len(errs) == 0 {
		system.PowerUseSummary = p
	}
	return warn, errs
}

// parseAppSystemPowerUseItem parses "pwi" (POWER_USE_ITEM_DATA) in App and System's PowerUseItem.
// We need to match raw item name with proto item name, so we parse manually.
// format: 8,0,u,pwi,unacc,2277
// drain type, power in mAh
// If drain type is "uid", it's per-app data, we add power in mAh into its app proto
// If drain type matches other types specified in "powerUseItemNameMap", we add
// it to system proto.
// If app has a PowerUseItem already, then newly found values from parsing will be added to it.
// The most common case of this would be when an app is installed on a device with multiple users
// (ie. with a work profile). In such cases, the app uid is combined with the user ID (to create
// UIDs such as 1010011 vs 10011) and thus the app is treated and reported separately for each profile.
// TODO: we currently combine them all under the same UID. In the future, we should separate them.
// The sum of app consumed power will be added to system proto later.
func parseAppSystemPowerUseItem(pc checkinutil.Counter, record []string, app *bspb.BatteryStats_App, system *bspb.BatteryStats_System, allAppComputedPowerMah *float32) (string, error) {
	var rawName string
	var computedPowerMah float32
	if _, err := parseSlice(pc, powerUseItemData, record, &rawName, &computedPowerMah); err != nil {
		return "", err
	}
	if rawName == "uid" {
		*allAppComputedPowerMah += computedPowerMah
		pb := app.GetPowerUseItem()
		if pb == nil {
			pb = &bspb.BatteryStats_App_PowerUseItem{}
			app.PowerUseItem = pb
		}
		pb.ComputedPowerMah = proto.Float32(pb.GetComputedPowerMah() + computedPowerMah)
	} else if name, ok := powerUseItemNameMap[rawName]; ok {
		system.PowerUseItem = append(system.PowerUseItem,
			&bspb.BatteryStats_System_PowerUseItem{
				Name:             name.Enum(),
				ComputedPowerMah: proto.Float32(computedPowerMah),
			})
	} else {
		pc.Count("unknown-app-system-power-use-item-"+rawName, 1)
		return fmt.Sprintf("Unknown powerUseItem name %s", rawName), nil
	}
	return "", nil
}

// parseAppScheduledJob parses "jb" (JOB_DATA) in App.
//
// If there exists a scheduledJob with the same name, newly found values
// (e.g., TotalTimeMsec) from parsing will be accumulated to their corresponding fields in that
// scheduledJob. Otherwise, a new scheduledJob will be created.
//
// format: 9,10007,l,jb,com.google.android.gms/.gcm.nts.TaskExecutionService,14000,3,12000,2
// name, total time (msec), count
func parseAppScheduledJob(record []string, app *bspb.BatteryStats_App) (string, []error) {
	j := &bspb.BatteryStats_App_ScheduledJob{}
	warn, errs := parseLine(jobData, record, j)
	if len(errs) > 0 {
		return warn, errs
	}
	// Scrub PII from the job name.
	j.Name = proto.String(historianutils.ScrubPII(j.GetName()))

	for _, j1 := range app.ScheduledJob {
		if j1.GetName() == j.GetName() {
			if err := accumulate(j1, j); err != nil {
				return warn, []error{fmt.Errorf("problem accumulating scheduled job: %v", err)}
			}
			return warn, nil
		}
	}

	// Job isn't in app's list of scheduled jobs so we add it as a new one
	app.ScheduledJob = append(app.ScheduledJob, j)
	return warn, nil
}

// parseAppStateTime parses "st" (STATE_TIME_DATA) in App.
//
// If the app already has state time data, newly found values from parsing will be accumulated to
// their corresponding fields.
//
// format:
// 9,10007,l,st,10,20,30 (report version <= 16)
//   [foreground time (ms), active time (ms), running/cached time (ms)]
// 9,10007,l,st,10,20,30,40,50,60 (report version >= 17)
//   [top time, foreground service time, top sleeping time, foreground time, background time, cached time]
func parseAppStateTime(pc checkinutil.Counter, record []string, reportVersion int32, app *bspb.BatteryStats_App) (string, []error) {
	if app.GetStateTime() == nil {
		app.StateTime = &bspb.BatteryStats_App_StateTime{}
	}
	if reportVersion < 17 {
		return parseAndAccumulate(stateTimeData, record, app.GetStateTime())
	}
	var fore, cached float32 // Proto backwards compatibility...these were float32 before.
	var top, foreServ, topSleep, back int64
	rem, err := parseSlice(pc, stateTimeData, record, &top, &foreServ, &topSleep, &fore, &back, &cached)
	var warn string
	if len(rem) > 0 {
		warn = fmt.Sprintf("%s has %d new fields", stateTimeData, len(rem))
	}
	if err != nil {
		return warn, []error{err}
	}

	s := app.GetStateTime()
	s.TopTimeMsec = proto.Int64(s.GetTopTimeMsec() + top)
	s.ForegroundServiceTimeMsec = proto.Int64(s.GetForegroundServiceTimeMsec() + foreServ)
	s.TopSleepingTimeMsec = proto.Int64(s.GetTopSleepingTimeMsec() + topSleep)
	s.ForegroundTimeMsec = proto.Float32(s.GetForegroundTimeMsec() + fore)
	s.BackgroundTimeMsec = proto.Int64(s.GetBackgroundTimeMsec() + back)
	s.CachedTimeMsec = proto.Float32(s.GetCachedTimeMsec() + cached)

	return warn, nil
}

// parseAppSync parses "sy" (SYNC_DATA) in App.
// If there exists a sync with the same name, newly found values (e.g. TotalTimeMsec) from
// parsing will be added to their corresponding fields in that sync. Otherwise, a new sync
// will be created.
//
// format: 8,10007,l,sy,com.google.android.gms.games/com.google/...@gmail.com,2161,4
// name, total time locked, count
func parseAppSync(record []string, app *bspb.BatteryStats_App) (string, []error) {
	s := &bspb.BatteryStats_App_Sync{}
	warn, plErrs := parseLine(syncData, record, s)
	if len(plErrs) > 0 {
		return warn, plErrs
	}
	// Scrub PII from the sync name
	s.Name = proto.String(historianutils.ScrubPII(s.GetName()))
	for _, s1 := range app.Sync {
		if s1.GetName() == s.GetName() {
			if err := accumulate(s1, s); err != nil {
				return warn, []error{fmt.Errorf("problem accumulating sync: %v", err)}
			}
			return warn, nil
		}
	}
	// Sync wasn't found in app's list of syncs so add it as a new one
	app.Sync = append(app.Sync, s)
	return warn, nil
}

// parseAppWakelock parses "wl" (WAKELOCK_DATA) in App.
// If there exists a wakelock with the same name, newly found values (e.g. FullTimeMsec)
// from parsing will be added to their corresponding fields in that wakelock.
// Otherwise, a new wakelock will be created.
//
// format (v18-): 8,1000,l,wl,ConnectivityService,0,f,0,15411273,p,263,0,w,0
// name, full wakelock time,    "f" (for full),    full wakelock count,
//       partial wakelock time, "p" (for partial), partial wakelock count
//       window wakelock time,  "w" (for window),  window wakelock count
// format (v19+): 9,1000,l,wl,ConnectivityService,0,f,0,-1,-1,15411273,p,263,5,10,0,w,0,-1,-1
// name, full wakelock time,    "f" (for full),    full wakelock count, full wakelock current duration, full wakelock max duration
//       partial wakelock time, "p" (for partial), partial wakelock count, partial wakelock current duration, partial wakelock max duration
//       window wakelock time,  "w" (for window),  window wakelock count, window wakelock current duration, window wakelock max duration
// format (v21+): 9,1000,l,wl,ConnectivityService,0,f,0,-1,-1,-1,15411273,p,263,5,10,25411273,0,w,0,-1,-1,-1
// name, full wakelock time,    "f" (for full),    full wakelock count, full wakelock current duration, full wakelock max duration, full wakelock total duration
//       partial wakelock time, "p" (for partial), partial wakelock count, partial wakelock current duration, partial wakelock max duration, partial wakelock total duration
//       window wakelock time,  "w" (for window),  window wakelock count, window wakelock current duration, window wakelock max duration, window wakelock total duration
//   [Note: wakelock total duration differs from wakelock time in that the latter is pooled (blame is shared amongst all apps), whereas the former is not.]
func parseAppWakelock(c checkinutil.Counter, reportVersion int32, record []string, app *bspb.BatteryStats_App) (string, error) {
	var ft, fc, pt, pc, wt, wc float32 // Proto backwards compatibility...these were float32 before.
	var fMax, fCur, fTot, pMax, pCur, pTot, wMax, wCur, wTot int64
	var name, warn string
	var rem []string
	var err error
	if reportVersion <= 18 {
		// The line contains letters that represent wakelock types, we skip those fields.
		rem, err = parseSlice(c, wakelockData, record, &name, &ft, nil /*"f"*/, &fc, &pt, nil /*"p"*/, &pc, &wt, nil /*"w"*/, &wc)
	} else if reportVersion <= 20 {
		// The line contains letters that represent wakelock types, we skip those fields.
		rem, err = parseSlice(c, wakelockData, record, &name,
			&ft, nil /*"f"*/, &fc, &fCur, &fMax,
			&pt, nil /*"p"*/, &pc, &pCur, &pMax,
			&wt, nil /*"w"*/, &wc, &wCur, &wMax)
	} else { // reportVersion >= 21
		// The line contains letters that represent wakelock types, we skip those fields.
		rem, err = parseSlice(c, wakelockData, record, &name,
			&ft, nil /*"f"*/, &fc, &fCur, &fMax, &fTot,
			&pt, nil /*"p"*/, &pc, &pCur, &pMax, &pTot,
			&wt, nil /*"w"*/, &wc, &wCur, &wMax, &wTot)
	}
	if len(rem) > 0 {
		warn = fmt.Sprintf("%s has %d new fields", wakelockData, len(rem))
	}
	if err != nil {
		return warn, err
	}
	// Scrub the PII from the wakelock data
	name = historianutils.ScrubPII(name)

	// In the case that the device has multiple profiles, and the same app holds the same wakelock in more than one profile, we amalgamate the data.
	for _, w1 := range app.Wakelock {
		if w1.GetName() == name {
			w1.FullTimeMsec = proto.Float32(w1.GetFullTimeMsec() + ft)
			w1.FullCount = proto.Float32(w1.GetFullCount() + fc)
			// Current and max should only track the longest value for the wakelock, so take the maximum, rather than the sum, of the data.
			w1.FullCurrentDurationMsec = proto.Int64(historianutils.MaxInt64(w1.GetFullCurrentDurationMsec(), fCur))
			w1.FullMaxDurationMsec = proto.Int64(historianutils.MaxInt64(w1.GetFullMaxDurationMsec(), fMax))
			// Total duration is summed (rather than taking the maximum), although neither are optimal. The total duration can exceed the bug report duration if there are multiple profiles.
			if fTot != -1 { // if not tracked, could be -1. In that case, don't sum -1s; just leave the original value (0 or -1).
				w1.FullTotalDurationMsec = proto.Int64(w1.GetFullTotalDurationMsec() + fTot)
			}
			w1.PartialTimeMsec = proto.Float32(w1.GetPartialTimeMsec() + pt)
			w1.PartialCount = proto.Float32(w1.GetPartialCount() + pc)
			// Current and max should only track the longest value for the wakelock, so take the maximum, rather than the sum, of the data.
			w1.PartialCurrentDurationMsec = proto.Int64(historianutils.MaxInt64(w1.GetPartialCurrentDurationMsec(), pCur))
			w1.PartialMaxDurationMsec = proto.Int64(historianutils.MaxInt64(w1.GetPartialMaxDurationMsec(), pMax))
			// Total duration is summed (rather than taking the maximum), although neither are optimal. The total duration can exceed the bug report duration if there are multiple profiles.
			if pTot != -1 { // if not tracked, could be -1. In that case, don't sum -1s; just leave the original value (0 or -1).
				w1.PartialTotalDurationMsec = proto.Int64(w1.GetPartialTotalDurationMsec() + pTot)
			}
			w1.WindowTimeMsec = proto.Float32(w1.GetWindowTimeMsec() + wt)
			w1.WindowCount = proto.Float32(w1.GetWindowCount() + wc)
			// Current and max should only track the longest value for the wakelock, so take the maximum, rather than the sum, of the data.
			w1.WindowCurrentDurationMsec = proto.Int64(historianutils.MaxInt64(w1.GetWindowCurrentDurationMsec(), wCur))
			w1.WindowMaxDurationMsec = proto.Int64(historianutils.MaxInt64(w1.GetWindowMaxDurationMsec(), wMax))
			// Total duration is summed (rather than taking the maximum), although neither are optimal. The total duration can exceed the bug report duration if there are multiple profiles.
			if wTot != -1 { // if not tracked, could be -1. In that case, don't sum -1s; just leave the original value (0 or -1).
				w1.WindowTotalDurationMsec = proto.Int64(w1.GetWindowTotalDurationMsec() + wTot)
			}

			return warn, nil
		}
	}

	// Wakelock wasn't found in app's list of wakelocks so add it as a new one.
	app.Wakelock = append(app.Wakelock, &bspb.BatteryStats_App_Wakelock{
		Name:                       proto.String(name),
		FullTimeMsec:               proto.Float32(ft),
		FullCount:                  proto.Float32(fc),
		FullCurrentDurationMsec:    proto.Int64(fCur),
		FullMaxDurationMsec:        proto.Int64(fMax),
		FullTotalDurationMsec:      proto.Int64(fTot),
		PartialTimeMsec:            proto.Float32(pt),
		PartialCount:               proto.Float32(pc),
		PartialCurrentDurationMsec: proto.Int64(pCur),
		PartialMaxDurationMsec:     proto.Int64(pMax),
		PartialTotalDurationMsec:   proto.Int64(pTot),
		WindowTimeMsec:             proto.Float32(wt),
		WindowCount:                proto.Float32(wc),
		WindowCurrentDurationMsec:  proto.Int64(wCur),
		WindowMaxDurationMsec:      proto.Int64(wMax),
		WindowTotalDurationMsec:    proto.Int64(wTot),
	})
	return warn, nil
}

// parseAppWakeupAlarm parses "wua" (WAKEUP_ALARM_DATA) in App.
// If there exists a wakeup alarm with the same name, newly found values (e.g. count)
// from parsing will be added to their corresponding fields in that wakeup alarm.
// Otherwise, a new wakeup alarm entry will be created.
//
// format: 9,1000,l,wua,*walarm*:JobScheduler.delay,17
// name, count
func parseAppWakeupAlarm(record []string, app *bspb.BatteryStats_App) (string, []error) {
	w := &bspb.BatteryStats_App_WakeupAlarm{}
	warn, errs := parseLine(wakeupAlarmData, record, w)
	if len(errs) > 0 {
		return warn, errs
	}
	w.Name = proto.String(historianutils.ScrubPII(w.GetName()))

	for _, w1 := range app.WakeupAlarm {
		if w1.GetName() == w.GetName() {
			if err := accumulate(w1, w); err != nil {
				return warn, []error{fmt.Errorf("problem accumulating wakeup alarm: %v", err)}
			}
			return warn, nil
		}
	}

	// Wakeup alarm wasn't found in app's current list, so add it as a new one.
	app.WakeupAlarm = append(app.WakeupAlarm, w)
	return warn, nil
}

// parseAppNetwork parses "nt" (NETWORK_DATA) into App.
//
// record holds content from BatteryStats's networkData.
//   e.g., 0,0,996,1389 (reportVersion < 8)
//   e.g., 0,0,8987,7051,0,0,25,29,0,0 (reportVersion >= 8)
// If app has a Network field already, then newly found values from parsing will be added to it.
// The most common case of this would be when an app is installed on a device with multiple users
// (ie. with a work profile). In such cases, the app uid is combined with the user ID (to create
// UIDs such as 1010011 vs 10011) and thus the app is treated and reported separately for each profile.
func parseAppNetwork(record []string, app *bspb.BatteryStats_App) (string, []error) {
	n := &bspb.BatteryStats_App_Network{}
	warn, errs := parseLine(networkData, record, n)
	if len(errs) > 0 {
		return warn, errs
	}
	// MobileActiveTime is output in microseconds in the log, so we convert to milliseconds here.
	n.MobileActiveTimeMsec = proto.Float32(n.GetMobileActiveTimeMsec() / 1e3)

	pb := app.GetNetwork()
	if pb == nil {
		app.Network = n
		return warn, nil
	}

	if err := accumulate(pb, n); err != nil {
		errs = append(errs, err)
	}
	return warn, errs
}

// parseAppWifi parses "wfl" (WIFI_DATA) in App.
// Parsing manually due to unit conversion. If app has a Wifi field already,
// then newly found values from parsing will be added to it.
// format: 14,10009,l,wfl,1386709324,304313000,0,7,3000,1500,500,5,2234567,1234567
// full wifi lock on time (usec), wifi scan time (usec), app wifi running time (usec), wifi scan count, wifi idle time (msec), wifi Rx time (msec), wifi Tx time (msec),
// background wifi scan count, wifi scan actual time (msec), background wifi scan actual time (msec)
func parseAppWifi(record []string, app *bspb.BatteryStats_App) (string, []error) {
	if len(record) < 3 {
		return "", []error{fmt.Errorf("%s line didn't contain enough fields", wifiData)}
	}

	w := &bspb.BatteryStats_App_Wifi{}
	warn, errs := parseLine(wifiData, record, w)
	if len(errs) > 0 {
		return warn, errs
	}

	// fullWifiLockTime, scanTime, runningTime are reported in usec, not msec, we convert them to msec here
	w.FullWifiLockTimeMsec = proto.Float32(w.GetFullWifiLockTimeMsec() / 1e3)
	w.ScanTimeMsec = proto.Float32(w.GetScanTimeMsec() / 1e3)
	w.RunningTimeMsec = proto.Float32(w.GetRunningTimeMsec() / 1e3)

	pb := app.GetWifi()
	if pb == nil {
		app.Wifi = w
		return warn, nil
	}

	if err := accumulate(pb, w); err != nil {
		errs = append(errs, err)
	}
	return warn, errs
}

// parseControllerData parses any output that describes controller activity.
//
// format: <idle_time>, <rx_time>, <power_ma_ms>, tx_time...
func parseControllerData(pc checkinutil.Counter, section string, record []string) (*bspb.BatteryStats_ControllerActivity, error) {
	var idle, rx, pwr int64
	rem, err := parseSlice(pc, section, record, &idle, &rx, &pwr)
	if err != nil {
		return nil, err
	}
	if len(rem) == 0 {
		return nil, fmt.Errorf(`%s didn't contain any transmit level data: "%v"`, section, record)
	}
	c := &bspb.BatteryStats_ControllerActivity{
		IdleTimeMsec: proto.Int64(idle),
		RxTimeMsec:   proto.Int64(rx),
		PowerMah:     proto.Int64(pwr),
	}
	for i, t := range rem {
		tm, err := strconv.Atoi(t)
		if err != nil {
			return nil, fmt.Errorf("%s contained invalid transmit value: %v", section, err)
		}
		c.Tx = append(c.Tx, &bspb.BatteryStats_ControllerActivity_TxLevel{
			Level:    proto.Int32(int32(i)),
			TimeMsec: proto.Int64(int64(tm)),
		})
	}
	return c, nil
}

// parseAndAccumulateControllerData parses any output that describes controller activity and
// adds the values to the existing proto. The input proto must not be nil.
//
// format: <idle_time>, <rx_time>, <power_ma_ms>, tx_time...
func parseAndAccumulateControllerData(pc checkinutil.Counter, section string, record []string, d *bspb.BatteryStats_ControllerActivity) error {
	if d == nil {
		return fmt.Errorf("provided nil ControllerActivity proto for %q", section)
	}
	c, err := parseControllerData(pc, section, record)
	if err != nil {
		return err
	}
	if len(d.Tx) != len(c.Tx) {
		return fmt.Errorf("%s had different number of transmit levels: %d vs %d", section, len(d.Tx), len(c.Tx))
	}
	d.IdleTimeMsec = proto.Int64(d.GetIdleTimeMsec() + c.GetIdleTimeMsec())
	d.RxTimeMsec = proto.Int64(d.GetRxTimeMsec() + c.GetRxTimeMsec())
	d.PowerMah = proto.Int64(d.GetPowerMah() + c.GetPowerMah())
	l := make(map[int32]int64)
	for _, t := range c.Tx {
		l[t.GetLevel()] = t.GetTimeMsec()
	}
	for _, t := range d.Tx {
		t.TimeMsec = proto.Int64(t.GetTimeMsec() + l[t.GetLevel()])
	}
	return nil
}

// parseSystemKernelWakelock parses "kwl" (KERNEL_WAKELOCK_DATA) in system
//
// format: 8,0,l,kwl,ipc000000b0_sensors.qcom,0,0
// wakelock name, time, count
func parseSystemKernelWakelock(record []string, system *bspb.BatteryStats_System) (string, []error) {
	w := &bspb.BatteryStats_System_KernelWakelock{}
	warn, plErrs := parseLine(kernelWakelockData, record, w)
	if len(plErrs) > 0 {
		return warn, plErrs
	}

	for _, kw := range system.KernelWakelock {
		if kw.GetName() == w.GetName() {
			kw.TimeMsec = proto.Float32(kw.GetTimeMsec() + w.GetTimeMsec())
			kw.Count = proto.Float32(kw.GetCount() + w.GetCount())
			// Current and max should only track the longest value for the wakelock.
			kw.CurrentDurationMsec = proto.Int64(historianutils.MaxInt64(kw.GetCurrentDurationMsec(), w.GetCurrentDurationMsec()))
			kw.MaxDurationMsec = proto.Int64(historianutils.MaxInt64(kw.GetMaxDurationMsec(), w.GetMaxDurationMsec()))
			return warn, nil
		}
	}

	// The kernel wakelock wasn't found in system's list of kernel wakelocks so add it as a new one.
	system.KernelWakelock = append(system.KernelWakelock, w)
	return warn, nil
}

// parseSystemMisc parses "m" (MISC_DATA) in System.
// format:
// 9,0,l,m,12469,0,20657343842,0,0,0,11258,0,0,3000,2,5000,1,2,1000,10000,10,15000,10,5000,3000 (reportVersion >= 16)
// 9,0,l,m,12469,0,20657343842,0,0,0,11258,0,0,3000,2,5000,1 (reportVersion >= 14)
// 9,0,l,m,12469,0,228195853,228195672,0,0,0,8889296,3246978,0,20657343842,0,0,0,11258,0,0 (8 <= reportVersion < 14)
// 8,0,l,m,47452,0,19133321,19133231,0,0,0,1863222,1605056,0,918161,0 (reportVersion < 8)
//
// screen on time, phone on time, [wifi on time, wifi running time,
// bluetooth on time, mobile rx total bytes, mobile tx total bytes,
// wifi rx total bytes, wifi tx total bytes, legacy input event count(always 0)]
// mobile radio active time, mobile radio active adjusted time
// low power mode enabled time, [# connectivity changes],
// [device idle mode enabled time, device idle mode enabled count,
// device idling time, device idling count], [mobile_active_count, mobile_active_unknown_time],
// [light idle mode enabled time, light idle mode enabled count, light idling time, light idling count,
// longest light idle mode enabled time, longest idle mode enabled time]
func parseSystemMisc(pc checkinutil.Counter, reportVersion int32, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetMisc() != nil {
		pc.Count("error-parse-system-misc-exist", 1)
		return "", []error{errors.New("misc field already exists")}
	}

	m := &bspb.BatteryStats_System_Misc{}
	// screen off time is not part of the line(calculated by subtracting screen
	// on time from battery real time. We put a dummy value in the input slice,
	// so we can still parse the other fields in the format specified by proto.
	record = append(record[:1], append([]string{"0"}, record[1:]...)...)

	var warn string
	var errs []error
	if reportVersion >= 14 {
		// Legacy data was removed from the line in version 14. Adding
		// dummy values so we can continue using predefined functions.
		record = append(record[:3], append([]string{"0", "0", "0", "0", "0", "0", "0"}, record[3:]...)...)
		warn, errs = parseLine(miscData, record, m)
	} else {
		warn, errs = parseLineWithSkip(miscData, record, m, []int{12} /* legacy input */)
	}

	if len(errs) == 0 {
		if reportVersion < 9 { // due to change in android client side
			m.FullWakelockTimeMsec = proto.Float32(m.GetFullWakelockTimeMsec() / 1e3)
			m.PartialWakelockTimeMsec = proto.Float32(m.GetPartialWakelockTimeMsec() / 1e3)
			m.MobileActiveTimeMsec = proto.Float32(m.GetMobileActiveTimeMsec() / 1e3)
			m.MobileActiveAdjustedTimeMsec = proto.Float32(m.GetMobileActiveAdjustedTimeMsec() / 1e3)
		}

		system.Misc = m
	}
	return warn, errs
}

// parseSystemBatteryDischarge parses "dc" (BATTERY_DISCHARGE_DATA) in System.
//
// record holds content from BatteryStats's batteryDischargeData.
//   e.g., 17,17,8,9
func parseSystemBatteryDischarge(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetBatteryDischarge() != nil {
		pc.Count("error-parse-system-battery-discharge-exist", 1)
		return "", []error{errors.New("battery discharge field already exists")}
	}
	b := &bspb.BatteryStats_System_BatteryDischarge{}
	warn, errs := parseLine(batteryDischargeData, record, b)
	if len(errs) == 0 {
		system.BatteryDischarge = b
	}
	return warn, errs
}

// parseSystemBatteryLevel parses "lv" (BATTERY_LEVEL_DATA) in System.
//
// record holds content from BatteryStats's batteryLevelData.
//   e.g., 100,83
func parseSystemBatteryLevel(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetBatteryLevel() != nil {
		pc.Count("error-parse-system-battery-level-exist", 1)
		return "", []error{errors.New("battery level field already exists")}
	}
	b := &bspb.BatteryStats_System_BatteryLevel{}
	warn, errs := parseLine(batteryLevelData, record, b)
	if len(errs) > 0 {
		return warn, errs
	}
	system.BatteryLevel = b
	return warn, nil
}

// parseGlobalBluetooth parses "gble" (GLOBAL_BLUETOOTH_DATA") into system, for report version < 17.
// format: 9,0,l,gble,15,16,17,18
// bluetooth_idle_time_msec, bluetooth_rx_time_msec, bluetooth_tx_time_msec, bluetooth_power_mah
func parseGlobalBluetooth(record []string, system *bspb.BatteryStats_System) (string, []error) {
	g := &bspb.BatteryStats_System_GlobalBluetooth{}
	warn, errs := parseLine(globalBluetoothControllerData, record, g)
	if len(errs) == 0 {
		system.GlobalBluetooth = g
	}
	return warn, errs
}

// parseGlobalNetwork parses "gn" (GLOBAL_NETWORK_DATA") into system.
// format: 0,0,1863222,1605056,0,0,5912,4257
func parseGlobalNetwork(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetGlobalNetwork() != nil {
		pc.Count("error-parse-system-global-network-exist", 1)
		return "", []error{errors.New("global network field already exists")}
	}
	n := &bspb.BatteryStats_System_GlobalNetwork{}
	warn, errs := parseLine(globalNetworkData, record, n)
	if len(errs) == 0 {
		system.GlobalNetwork = n
	}
	return warn, errs
}

// parseGlobalWifi parses "gwfl" (GLOBAL_WIFI_DATA") into system.
// format: 9,0,l,gwfl,9,10,11,12,13,14
// wifi_on_time_msec, wifi_running_time_msec, wifi_idle_time_msec, wifi_rx_time_msec, wifi_tx_time_msec, wifi_power_mah
func parseGlobalWifi(record []string, system *bspb.BatteryStats_System) (string, []error) {
	g := &bspb.BatteryStats_System_GlobalWifi{}
	warn, errs := parseLine(globalWifiData, record, g)
	if len(errs) == 0 {
		system.GlobalWifi = g
	}
	return warn, errs
}

// parseSystemScreenBrightness parses "br" (SCREEN_BRIGHTNESS_DATA) in System.
// format: 8,0,l,br,0,0,56369,0,0
// time spent from level 0 to 4
// (0: DARK, ..., 4:BRIGHT, see proto for details)
func parseSystemScreenBrightness(c checkinutil.Counter, record []string, system *bspb.BatteryStats_System) error {
	if len(system.GetScreenBrightness()) > 0 {
		c.Count("error-system-screen-brightness-exist", 1)
		return errors.New("screen brightness field already exists")
	}
	if len(record) != len(bspb.BatteryStats_System_ScreenBrightness_Name_name) {
		c.Count("error-system-screen-brightness-wrong-number-of-records", 1)
		return fmt.Errorf("wrong number of screen brightness fields: got %d, want %d", len(record), len(bspb.BatteryStats_System_ScreenBrightness_Name_name))
	}
	for name, rawTimeMsec := range record {
		timeMsec, err := parseFloat32(rawTimeMsec)
		if err != nil {
			c.Count("error-parse-system-screen-brightness", 1)
			return err
		}
		system.ScreenBrightness = append(system.ScreenBrightness,
			&bspb.BatteryStats_System_ScreenBrightness{
				Name:     bspb.BatteryStats_System_ScreenBrightness_Name(name).Enum(),
				TimeMsec: proto.Float32(timeMsec),
			})
	}
	return nil
}

// parseSystemSignalScanningTime parses "sst" (SIGNAL_SCANNING_TIME_DATA) in system.
//
// format: 9,0,l,sst,9000
func parseSystemSignalScanningTime(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetSignalScanningTime() != nil {
		pc.Count("error-parse-system-signal-scanning-time-exist", 1)
		return "", []error{errors.New("signal scanning time field already exists")}
	}
	s := &bspb.BatteryStats_System_SignalScanningTime{}
	warn, errs := parseLine(signalScanningTimeData, record, s)
	if len(errs) == 0 {
		system.SignalScanningTime = s
	}
	return warn, errs
}

// parseSystemTimeCountPair parses categories that have time count pairs (in two separate lines in
// the checkin data).
//
// format: time ends with "t", count ends with "c"
// each has five values corresponds to five levels
// (0: NONE OR UNKNOW, ..., 4: GREAT, see proto for details)
// 8,0,l,sgt,2307235,8838772,22120797,18900758,0
// 8,0,l,sgc,19,60,85,38,1
//
// record holds content from one of the following sections in BatteryStats:
//   bluetoothStateTimeData, bluetoothStateCountData,
//   dataConnectionTimeData, dataConnectionCountData,
//   signalStrengthTimeData, signalStrengthCountData,
//   wifiSignalStrengthTimeData, wifiSignalStrengthCountData,
//   wifiStateTimeData, wifiStateCountData,
//   wifiSupplicantStateTimeData, wifiSupplicantStateCountData
//
// Note the sub-message (e.g., system.SignalStrength) is a repeated field whose i-th element
// corresponds to i-th element in record, and the index "i" here matches i-th Name (enum type) in
// each sub-message. Specifically, for any sub-message M (e.g., system.SignalStrength):
//   For any M.Name enum i:
//     system.M[i].Name = i
//     system.M[i].TimeMsec (or Count) = record[i]
//
// These sub-messages are not based on a single proto definition despite their similarity because
// there's no guarantee that the input BatteryStats data (csv file) from Android client will stick
// to this similarity. (e.g., a new field may be added only for wifiStateTimeData, which would
// incur data migration if a single proto were used). This function just aims to reuse some common
// code for such sub-messages.
func parseSystemTimeCountPair(c checkinutil.Counter, section string, record []string, system *bspb.BatteryStats_System) (string, error) {
	dataLen := len(record)
	var currentLen, expectedLen int
	switch section {
	case signalStrengthTimeData, signalStrengthCountData:
		currentLen = len(system.SignalStrength)
		expectedLen = len(bspb.BatteryStats_System_SignalStrength_Name_name)
	case dataConnectionTimeData, dataConnectionCountData:
		currentLen = len(system.DataConnection)
		expectedLen = len(bspb.BatteryStats_System_DataConnection_Name_name)
	case wifiStateTimeData, wifiStateCountData:
		currentLen = len(system.WifiState)
		expectedLen = len(bspb.BatteryStats_System_WifiState_Name_name)
	case bluetoothStateTimeData, bluetoothStateCountData:
		currentLen = len(system.BluetoothState)
		expectedLen = len(bspb.BatteryStats_System_BluetoothState_Name_name)
	case wifiSupplStateTimeData, wifiSupplStateCountData:
		currentLen = len(system.WifiSupplicantState)
		expectedLen = len(bspb.BatteryStats_System_WifiSupplicantState_Name_name)
	case wifiSignalStrengthTimeData, wifiSignalStrengthCountData:
		currentLen = len(system.WifiSignalStrength)
		expectedLen = len(bspb.BatteryStats_System_WifiSignalStrength_Name_name)
	default:
		return "", fmt.Errorf("parseSystemTimeCountPair encountered unknown section: %s", section) // Shouldn't happen since we specifically filter which sections get sent to this function.
	}

	warning := ""
	if dataLen < expectedLen {
		c.Count("error-system-"+section+"-too-few-records", 1)
		return "", fmt.Errorf("%s doesn't contain enough records", section)
	} else if dataLen > expectedLen {
		c.Count("error-system-"+section+"-too-many-records", 1)
		warning = fmt.Sprintf("%s has %d additional field(s) that are not captured.", section, dataLen-expectedLen)
	}

	if currentLen == 0 { // No proto exists. Create a new proto and fill the field.
		for name, rawValue := range record {
			value, err := parseFloat32(rawValue)
			if err != nil {
				return warning, err
			}
			switch section {
			case signalStrengthTimeData:
				system.SignalStrength = append(system.SignalStrength,
					&bspb.BatteryStats_System_SignalStrength{
						Name:     bspb.BatteryStats_System_SignalStrength_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case signalStrengthCountData:
				system.SignalStrength = append(system.SignalStrength,
					&bspb.BatteryStats_System_SignalStrength{
						Name:  bspb.BatteryStats_System_SignalStrength_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case dataConnectionTimeData:
				system.DataConnection = append(system.DataConnection,
					&bspb.BatteryStats_System_DataConnection{
						Name:     bspb.BatteryStats_System_DataConnection_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case dataConnectionCountData:
				system.DataConnection = append(system.DataConnection,
					&bspb.BatteryStats_System_DataConnection{
						Name:  bspb.BatteryStats_System_DataConnection_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case wifiStateTimeData:
				system.WifiState = append(system.WifiState,
					&bspb.BatteryStats_System_WifiState{
						Name:     bspb.BatteryStats_System_WifiState_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case wifiStateCountData:
				system.WifiState = append(system.WifiState,
					&bspb.BatteryStats_System_WifiState{
						Name:  bspb.BatteryStats_System_WifiState_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case bluetoothStateTimeData:
				system.BluetoothState = append(system.BluetoothState,
					&bspb.BatteryStats_System_BluetoothState{
						Name:     bspb.BatteryStats_System_BluetoothState_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case bluetoothStateCountData:
				system.BluetoothState = append(system.BluetoothState,
					&bspb.BatteryStats_System_BluetoothState{
						Name:  bspb.BatteryStats_System_BluetoothState_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case wifiSignalStrengthTimeData:
				system.WifiSignalStrength = append(system.WifiSignalStrength, &bspb.BatteryStats_System_WifiSignalStrength{
					Name:     bspb.BatteryStats_System_WifiSignalStrength_Name(name).Enum(),
					TimeMsec: proto.Float32(value),
				})
			case wifiSignalStrengthCountData:
				system.WifiSignalStrength = append(system.WifiSignalStrength, &bspb.BatteryStats_System_WifiSignalStrength{
					Name:  bspb.BatteryStats_System_WifiSignalStrength_Name(name).Enum(),
					Count: proto.Float32(value),
				})
			case wifiSupplStateTimeData:
				system.WifiSupplicantState = append(system.WifiSupplicantState, &bspb.BatteryStats_System_WifiSupplicantState{
					Name:     bspb.BatteryStats_System_WifiSupplicantState_Name(name).Enum(),
					TimeMsec: proto.Float32(value),
				})
			case wifiSupplStateCountData:
				system.WifiSupplicantState = append(system.WifiSupplicantState, &bspb.BatteryStats_System_WifiSupplicantState{
					Name:  bspb.BatteryStats_System_WifiSupplicantState_Name(name).Enum(),
					Count: proto.Float32(value),
				})
			default:
				return warning, fmt.Errorf("parseSystemTimeCountPair encountered unknown section: %s", section) // Shouldn't happen, explained above.
			}
		}
	} else if currentLen == len(record) { // The proto exists. Just fill the field.
		for name, rawValue := range record {
			value, err := parseFloat32(rawValue)
			if err != nil {
				return warning, err
			}
			var prev *float32
			switch section {
			case signalStrengthTimeData:
				prev = system.SignalStrength[name].TimeMsec
				system.SignalStrength[name].TimeMsec = proto.Float32(value)
			case signalStrengthCountData:
				prev = system.SignalStrength[name].Count
				system.SignalStrength[name].Count = proto.Float32(value)
			case dataConnectionTimeData:
				prev = system.DataConnection[name].TimeMsec
				system.DataConnection[name].TimeMsec = proto.Float32(value)
			case dataConnectionCountData:
				prev = system.DataConnection[name].Count
				system.DataConnection[name].Count = proto.Float32(value)
			case wifiStateTimeData:
				prev = system.WifiState[name].TimeMsec
				system.WifiState[name].TimeMsec = proto.Float32(value)
			case wifiStateCountData:
				prev = system.WifiState[name].Count
				system.WifiState[name].Count = proto.Float32(value)
			case bluetoothStateTimeData:
				prev = system.BluetoothState[name].TimeMsec
				system.BluetoothState[name].TimeMsec = proto.Float32(value)
			case bluetoothStateCountData:
				prev = system.BluetoothState[name].Count
				system.BluetoothState[name].Count = proto.Float32(value)
			case wifiSupplStateTimeData:
				prev = system.WifiSupplicantState[name].TimeMsec
				system.WifiSupplicantState[name].TimeMsec = proto.Float32(value)
			case wifiSupplStateCountData:
				prev = system.WifiSupplicantState[name].Count
				system.WifiSupplicantState[name].Count = proto.Float32(value)
			case wifiSignalStrengthTimeData:
				prev = system.WifiSignalStrength[name].TimeMsec
				system.WifiSignalStrength[name].TimeMsec = proto.Float32(value)
			case wifiSignalStrengthCountData:
				prev = system.WifiSignalStrength[name].Count
				system.WifiSignalStrength[name].Count = proto.Float32(value)
			default:
				return warning, fmt.Errorf("parseSystemTimeCountPair encountered unknown section: %s", section)
			}
			if prev != nil {
				c.Count("error-system-"+section+"-exist", 1)
				return warning, fmt.Errorf("%s section already exists", section)
			}
		}
	} else {
		return warning, fmt.Errorf("inconsistent number of fields in %s", section) // Shouldn't happen
	}
	return warning, nil
}

// parseSystemWakeupReason parses "wr" (WAKEUP_REASON_DATA) in System. These are low-level messages and don't contain PII.
//
// record holds content from BatteryStats's wakeupReasonData.
//   e.g., "289:bcmsdh_sdmmc:200:qcom,mpm:240:msmgpio",308628 (reportVersion < 11)
//   e.g., "289:bcmsdh_sdmmc:200:qcom,mpm:240:msmgpio",308628,399 (reportVersion >= 11)
func parseSystemWakeupReason(record []string, system *bspb.BatteryStats_System) (string, []error) {
	// This time is the amount of time we saw the CPU awake from when we received
	// a wake reason until we had another reason for it to be awake (someone
	// acquiring a user space wake lock or another sleep/wake happening).
	// 8,0,l,wr,"200:qcom,smd-rpm:222:fc4cf000.qcom,spmi",760
	wr := &bspb.BatteryStats_System_WakeupReason{}
	warn, errs := parseLine(wakeupReasonData, record, wr)
	if len(errs) > 0 {
		return warn, errs
	}
	system.WakeupReason = append(system.WakeupReason, wr)
	return warn, nil
}

// SystemBattery parses "bt" (BATTERY_DATA) in System.
//
// format:
//   7,0,l,bt,N/A,4946,4946,25430,25430 (reportVersion < 8)
//   9,0,l,bt,0,19447364,2268899,19466586,2288120,1411399763148,19399912,2221446 (8 <= reportVersion < 18)
//   9,0,l,bt,0,19447364,2268899,19466586,2288120,1411399763148,19399912,2221446,3000000 (18 <= reportVersion <= 20)
//   9,0,l,bt,0,19447364,2268899,19466586,2288120,1411399763148,19399912,2221446,3000000,3240000,3400000 (reportVersion >= 20)
func SystemBattery(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetBattery() != nil {
		pc.Count("error-parse-system-battery-exist", 1)
		return "", []error{errors.New("system battery field already exists")}
	}
	if record[0] == "N/A" {
		// Batterystats pipeline defaults startCount to 0 in the case of a N/A
		record = append([]string{"0"}, record[1:]...)
	}
	b := &bspb.BatteryStats_System_Battery{}
	warn, errs := parseLine(batteryData, record, b)
	if len(errs) == 0 {
		system.Battery = b
	}
	return warn, errs
}

// parseSlice wraps sliceparse.Consume(value, outputs...), increasing pc if it returns an error.
func parseSlice(pc checkinutil.Counter, name string, value []string, outputs ...interface{}) ([]string, error) {
	remaining, err := sliceparse.Consume(value, outputs...)
	if err != nil {
		pc.Count("error-parse-slice-"+name, 1)
	}
	return remaining, err
}

// parseFloat32 parses an individual 32-bit float.
func parseFloat32(s string) (float32, error) {
	f64, err := strconv.ParseFloat(s, 32)
	return float32(f64), err
}

func parseValue(src string, dstV reflect.Value) error {
	var err error
	srcSlice := []string{src}
	switch dstV.Kind() {
	case reflect.Float32:
		var val float32
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.Float64:
		var val float64
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.Int32:
		var val int32
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.Int64:
		var val int64
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.String:
		dstV.Set(reflect.ValueOf(src))
	default:
		return fmt.Errorf("parse error: type %s not supported", dstV.Kind().String())
	}
	return err
}

// parseAndAccumulate is a generic parsing function that parses a string slice into a proto,
// accumulating corresponding values in the input proto. The input proto must not be nil.
// This function cannot deal with nested protos.
func parseAndAccumulate(section string, record []string, p proto.Message) (string, []error) {
	if p == nil {
		return "", []error{fmt.Errorf("nil proto parsed for %s section", section)}
	}
	acc := reflect.ValueOf(p)

	// parse
	rec := reflect.New(acc.Type().Elem())
	warn, errs := parseLine(section, record, rec.Interface().(proto.Message))
	if len(errs) > 0 {
		return warn, errs
	}

	// and accumulate
	if err := accumulate(p, rec.Interface().(proto.Message)); err != nil {
		return warn, []error{fmt.Errorf("unable to accumulate: %v", err)}
	}
	return warn, nil
}

// accumulate adds corresponding values in the given protos and puts them into the first proto. The input proto must not be nil. This function cannot deal with nested protos.
func accumulate(p1, p2 proto.Message) error {
	if p1 == nil {
		return errors.New("nil proto1 passed")
	}
	if p2 == nil {
		return errors.New("nil proto2 passed")
	}
	r1, r2 := reflect.TypeOf(p1), reflect.TypeOf(p2)
	if r1 != r2 {
		// Just....just don't.
		return fmt.Errorf("mismatched types: %v vs %v", r1, r2)
	}

	acc := reflect.ValueOf(p1)
	rec := reflect.ValueOf(p2)
	ids := BatteryStatsIDMap[r1]

	for i := 0; i < rec.Elem().NumField(); i++ {
		if f := rec.Elem().Type().Field(i); strings.HasPrefix(f.Name, "XXX_") || f.PkgPath != "" {
			continue // skip XXX_ and unexported fields
		}
		if ids[i] {
			// This is a message ID. It should not be accumulated or changed in any way.
			continue
		}
		// pointer to field i. Golang requires the temporary variables in order for the rest of this processing to work.
		accPtr, recPtr := acc.Elem().Field(i), rec.Elem().Field(i)
		if accPtr.IsNil() {
			accPtr.Set(recPtr)
			continue
		}
		switch recPtr.Elem().Kind() {
		case reflect.Float32, reflect.Float64:
			accPtr.Elem().SetFloat(accPtr.Elem().Float() + recPtr.Elem().Float())
		case reflect.Int32, reflect.Int64:
			accPtr.Elem().SetInt(accPtr.Elem().Int() + recPtr.Elem().Int())
		}
	}

	return nil
}

// parseLineWithSkip skips string with specified index in record then uses parseLine
// to parse the remaining strings.
func parseLineWithSkip(section string, record []string, p proto.Message, skip []int) (string, []error) {
	sort.Ints(skip)
	next, nextSkip := 0, 0 // use next to traverse record, nextSkip to traverse skip.
	for i, s := range record {
		if nextSkip != -1 && i == skip[nextSkip] {
			nextSkip++
			if nextSkip == len(skip) {
				// stop skipping
				nextSkip = -1
			}
		} else {
			record[next] = s
			next++
		}
	}
	// Only need to pass the elements that were not skipped.
	return parseLine(section, record[:next], p)
}

// parseLine is a generic parsing function that parses a string slice into a proto.
// The proto should only have fields of type float32, float64, int32, int64 or string.
// The function determines the type a string should be parsed into by the type
// and order specified in the proto.
func parseLine(section string, record []string, p proto.Message) (string, []error) {
	var warn string
	var errs []error
	pV := reflect.ValueOf(p)
	// Because of backwards compatibility, there will be times when the proto
	// will have more fields than record can provide, so just fill up as many
	// fields as possible.
	num := len(record)
	if len(record) > pV.Elem().NumField()-1 {
		num = pV.Elem().NumField() - 1
		warn = fmt.Sprintf("The underlying format for %s has %d additional field(s) that are not captured.", section, len(record)-num)
	}

	for i := 0; i < num; i++ {
		valPtrV := reflect.New(pV.Elem().Field(i).Type().Elem())
		if err := parseValue(record[i], valPtrV.Elem()); err == nil {
			pV.Elem().Field(i).Set(valPtrV)
		} else {
			errs = append(errs, fmt.Errorf("error parsing %s: %v", section, err))
		}
	}
	return warn, errs
}
